From 827f63ea9ff22783f7753078a0cc200abece1eab Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 28 Oct 2025 15:32:49 +0200 Subject: [PATCH 001/313] Add tests for rule engine consumer loop --- ...faultTbCalculatedFieldConsumerService.java | 6 +- .../queue/DefaultTbCoreConsumerService.java | 6 +- .../queue/DefaultTbEdgeConsumerService.java | 6 +- .../DefaultTbRuleEngineConsumerService.java | 6 +- .../TbMsgPackProcessingContextFactory.java | 35 +++ .../processing/AbstractConsumerService.java | 6 +- .../AbstractTbRuleEngineSubmitStrategy.java | 12 +- .../BatchTbRuleEngineSubmitStrategy.java | 2 +- .../BurstTbRuleEngineSubmitStrategy.java | 2 +- .../service/queue/processing/IdMsgPair.java | 14 +- ...lByEntityIdTbRuleEngineSubmitStrategy.java | 10 +- .../SequentialTbRuleEngineSubmitStrategy.java | 6 +- .../TbRuleEngineQueueConsumerManager.java | 8 +- .../src/main/resources/thingsboard.yml | 3 + .../queue/TbMsgPackProcessingContextTest.java | 260 ++++++++++++++-- .../RuleEngineConsumerLoopTest.java | 281 ++++++++++++++++++ 16 files changed, 595 insertions(+), 68 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextFactory.java create mode 100644 application/src/test/java/org/thingsboard/server/service/queue/ruleengine/RuleEngineConsumerLoopTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index acb36449e8..91981cf86f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -146,15 +146,15 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa private void processMsgs(List> msgs, TbQueueConsumer> consumer, Object consumerKey, QueueConfig config) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); PendingMsgHolder pendingMsgHolder = new PendingMsgHolder<>(); Future packSubmitFuture = consumersExecutor.submit(() -> { orderedMsgList.forEach((element) -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); + UUID id = element.uuid(); + TbProtoQueueMsg msg = element.msg(); log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); TbCallback callback = new TbPackCallback<>(id, ctx); try { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 9ab5a062eb..57a7018310 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -260,15 +260,15 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, Object consumerKey, QueueConfig config) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); PendingMsgHolder pendingMsgHolder = new PendingMsgHolder<>(); Future packSubmitFuture = consumersExecutor.submit(() -> { orderedMsgList.forEach((element) -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); + UUID id = element.uuid(); + TbProtoQueueMsg msg = element.msg(); log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); TbCallback callback = new TbPackCallback<>(id, ctx); try { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java index 40ef9bfeca..71dacdbdaf 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java @@ -129,15 +129,15 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, Object consumerKey, QueueConfig edgeQueueConfig) throws InterruptedException { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); PendingMsgHolder pendingMsgHolder = new PendingMsgHolder<>(); Future submitFuture = consumersExecutor.submit(() -> { orderedMsgList.forEach((element) -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); + UUID id = element.uuid(); + TbProtoQueueMsg msg = element.msg(); TbCallback callback = new TbPackCallback<>(id, ctx); try { ToEdgeMsg toEdgeMsg = msg.getValue(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index a809f2bb11..d2a8e7a441 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -70,6 +70,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo private final TbRuleEngineConsumerContext ctx; private final QueueService queueService; private final TbRuleEngineDeviceRpcService tbDeviceRpcService; + private final TbMsgPackProcessingContextFactory packProcessingContextFactory; private final ConcurrentMap consumers = new ConcurrentHashMap<>(); @@ -85,11 +86,13 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo PartitionService partitionService, ApplicationEventPublisher eventPublisher, JwtSettingsService jwtSettingsService, - CalculatedFieldCache calculatedFieldCache) { + CalculatedFieldCache calculatedFieldCache, + TbMsgPackProcessingContextFactory packProcessingContextFactory) { super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); this.ctx = ctx; this.tbDeviceRpcService = tbDeviceRpcService; this.queueService = queueService; + this.packProcessingContextFactory = packProcessingContextFactory; } @Override @@ -255,6 +258,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo .consumerExecutor(consumersExecutor) .scheduler(scheduler) .taskExecutor(mgmtExecutor) + .packProcessingContextFactory(packProcessingContextFactory) .build(); consumers.put(queueKey, consumer); consumer.init(queue); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextFactory.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextFactory.java new file mode 100644 index 0000000000..ff804ad304 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextFactory.java @@ -0,0 +1,35 @@ +/** + * 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.queue; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; + +public interface TbMsgPackProcessingContextFactory { + + TbMsgPackProcessingContext create(String queueName, TbRuleEngineSubmitStrategy submitStrategy, boolean skipTimeouts); + + @Component + class DefaultTbMsgPackProcessingContextFactory implements TbMsgPackProcessingContextFactory { + + @Override + public TbMsgPackProcessingContext create(String queueName, TbRuleEngineSubmitStrategy submitStrategy, boolean skipTimeouts) { + return new TbMsgPackProcessingContext(queueName, submitStrategy, skipTimeouts); + } + + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index e18cc0b09e..7e9ada0e75 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -134,13 +134,13 @@ public abstract class AbstractConsumerService> msgs, TbQueueConsumer> consumer) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + Collectors.toConcurrentMap(IdMsgPair::uuid, IdMsgPair::msg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); TbPackProcessingContext> ctx = new TbPackProcessingContext<>( processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); orderedMsgList.forEach(element -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); + UUID id = element.uuid(); + TbProtoQueueMsg msg = element.msg(); log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue()); TbCallback callback = new TbPackCallback<>(id, ctx); try { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java index 019573631a..5a1bdae13a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java @@ -44,21 +44,21 @@ public abstract class AbstractTbRuleEngineSubmitStrategy implements TbRuleEngine @Override public ConcurrentMap> getPendingMap() { - return orderedMsgList.stream().collect(Collectors.toConcurrentMap(pair -> pair.uuid, pair -> pair.msg)); + return orderedMsgList.stream().collect(Collectors.toConcurrentMap(pair -> pair.uuid(), pair -> pair.msg())); } @Override public void update(ConcurrentMap> reprocessMap) { List> newOrderedMsgList = new ArrayList<>(reprocessMap.size()); for (IdMsgPair pair : orderedMsgList) { - if (reprocessMap.containsKey(pair.uuid)) { - if (StringUtils.isNotEmpty(pair.getMsg().getValue().getFailureMessage())) { - var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder(pair.getMsg().getValue()) + if (reprocessMap.containsKey(pair.uuid())) { + if (StringUtils.isNotEmpty(pair.msg().getValue().getFailureMessage())) { + var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder(pair.msg().getValue()) .clearFailureMessage() .clearRelationTypes() .build(); - var newMsg = new TbProtoQueueMsg<>(pair.getMsg().getKey(), toRuleEngineMsg, pair.getMsg().getHeaders()); - newOrderedMsgList.add(new IdMsgPair<>(pair.getUuid(), newMsg)); + var newMsg = new TbProtoQueueMsg<>(pair.msg().getKey(), toRuleEngineMsg, pair.msg().getHeaders()); + newOrderedMsgList.add(new IdMsgPair<>(pair.uuid(), newMsg)); } else { newOrderedMsgList.add(pair); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java index 93478eab18..a47c43422b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java @@ -73,7 +73,7 @@ public class BatchTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS pendingPack.clear(); for (int i = startIdx; i < endIdx; i++) { IdMsgPair pair = orderedMsgList.get(i); - pendingPack.put(pair.uuid, pair.msg); + pendingPack.put(pair.uuid(), pair.msg()); } tmpPack = new LinkedHashMap<>(pendingPack); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java index af465686e9..9f9dd0a60f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java @@ -34,7 +34,7 @@ public class BurstTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS if (log.isDebugEnabled()) { log.debug("[{}] submitting [{}] messages to rule engine", queueName, orderedMsgList.size()); } - orderedMsgList.forEach(pair -> msgConsumer.accept(pair.uuid, pair.msg)); + orderedMsgList.forEach(pair -> msgConsumer.accept(pair.uuid(), pair.msg())); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java index e74362699b..6795b2822f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/IdMsgPair.java @@ -15,19 +15,9 @@ */ package org.thingsboard.server.service.queue.processing; -import lombok.Getter; +import com.google.protobuf.GeneratedMessageV3; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import java.util.UUID; -public class IdMsgPair { - @Getter - final UUID uuid; - @Getter - final TbProtoQueueMsg msg; - - public IdMsgPair(UUID uuid, TbProtoQueueMsg msg) { - this.uuid = uuid; - this.msg = msg; - } -} +public record IdMsgPair(UUID uuid, TbProtoQueueMsg msg) {} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java index c72f1274e7..1aa1b20284 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java @@ -51,7 +51,7 @@ public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends Abs entityIdToListMap.forEach((entityId, queue) -> { IdMsgPair msg = queue.peek(); if (msg != null) { - msgConsumer.accept(msg.uuid, msg.msg); + msgConsumer.accept(msg.uuid(), msg.msg()); } }); } @@ -71,13 +71,13 @@ public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends Abs IdMsgPair next = null; synchronized (queue) { IdMsgPair expected = queue.peek(); - if (expected != null && expected.uuid.equals(id)) { + if (expected != null && expected.uuid().equals(id)) { queue.poll(); next = queue.peek(); } } if (next != null) { - msgConsumer.accept(next.uuid, next.msg); + msgConsumer.accept(next.uuid(), next.msg()); } } } @@ -87,9 +87,9 @@ public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends Abs msgToEntityIdMap.clear(); entityIdToListMap.clear(); for (IdMsgPair pair : orderedMsgList) { - EntityId entityId = getEntityId(pair.msg.getValue()); + EntityId entityId = getEntityId(pair.msg().getValue()); if (entityId != null) { - msgToEntityIdMap.put(pair.uuid, entityId); + msgToEntityIdMap.put(pair.uuid(), entityId); entityIdToListMap.computeIfAbsent(entityId, id -> new LinkedList<>()).add(pair); } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java index be97853310..c048f42ee0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java @@ -60,11 +60,11 @@ public class SequentialTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSu int idx = msgIdx.get(); if (idx < listSize) { IdMsgPair pair = orderedMsgList.get(idx); - expectedMsgId = pair.uuid; + expectedMsgId = pair.uuid(); if (log.isDebugEnabled()) { - log.debug("[{}] submitting [{}] message to rule engine", queueName, pair.msg); + log.debug("[{}] submitting [{}] message to rule engine", queueName, pair.msg()); } - msgConsumer.accept(pair.uuid, pair.msg); + msgConsumer.accept(pair.uuid(), pair.msg()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index d067be49a0..a6f7e34208 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -41,6 +41,7 @@ import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.TbMsgPackCallback; import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; @@ -66,13 +67,15 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager { Integer partitionId = tpi != null ? tpi.getPartition().orElse(-1) : null; @@ -81,6 +84,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager submitMessage(packCtx, id, msg)); final boolean timeout = !packCtx.await(queue.getPackProcessingTimeout(), TimeUnit.MILLISECONDS); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 61b797515a..aa0b5d7920 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1889,6 +1889,9 @@ queue: print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" # Max length of the error message that is printed by statistics max-error-message-length: "${TB_QUEUE_RULE_ENGINE_MAX_ERROR_MESSAGE_LENGTH:4096}" + prometheus-stats: + # Enable/disable Prometheus statistics for individual Rule Engine message processing (records time in ms for success/failure). + enabled: "${TB_QUEUE_RULE_ENGINE_PROMETHEUS_STATS_ENABLED:true}" # After a queue is deleted (or the profile's isolation option was disabled), Rule Engine will continue reading related topics during this period before deleting the actual topics topic-deletion-delay: "${TB_QUEUE_RULE_ENGINE_TOPIC_DELETION_DELAY_SEC:15}" # Size of the thread pool that handles such operations as partition changes, config updates, queue deletion diff --git a/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java index ef1febfa6c..b5d65ea035 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java @@ -15,14 +15,17 @@ */ package org.thingsboard.server.service.queue; -import lombok.extern.slf4j.Slf4j; -import org.junit.After; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; @@ -35,30 +38,241 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@Slf4j -@RunWith(MockitoJUnitRunner.class) -public class TbMsgPackProcessingContextTest { +@ExtendWith(MockitoExtension.class) +class TbMsgPackProcessingContextTest { + + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + + @Mock + TbRuleEngineSubmitStrategy submitStrategy; + @Mock + TbProtoQueueMsg mockMsg; + + ConcurrentMap> pendingMap; - public static final int TIMEOUT = 10; ExecutorService executorService; - @After - public void tearDown() { + @BeforeEach + void setup() { + pendingMap = new ConcurrentHashMap<>(); + lenient().when(submitStrategy.getPendingMap()).thenReturn(pendingMap); + } + + @AfterEach + void tearDown() { if (executorService != null) { - executorService.shutdownNow(); + MoreExecutors.shutdownAndAwaitTermination(executorService, 5, TimeUnit.SECONDS); } } @Test - public void testHighConcurrencyCase() throws InterruptedException { - //log.warn("preparing the test..."); + void testAwait_shouldReturnTrue_whenOnSuccessIsCalledBeforeTimeout() throws InterruptedException { + // GIVEN - a context with one pending message + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId = UUID.randomUUID(); + pendingMap.put(msgId, mockMsg); + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + // WHEN - onSuccess() is called in another thread before timeout + executorService.submit(() -> { + try { + Thread.sleep(100); + context.onSuccess(msgId); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN - await() should return true (successful completion) + boolean result = context.await(5000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should return true when latch is counted down before timeout").isTrue(); + + // Verify the message was moved to success map + assertThat(context.getSuccessMap()).containsKey(msgId); + assertThat(context.getPendingMap()).isEmpty(); + assertThat(context.getExceptionsMap()).isEmpty(); + + + // Verify submit strategy was notified about successful message processing + then(submitStrategy).should().onSuccess(msgId); + } + + @Test + void testAwait_shouldReturnTrue_whenOnFailureIsCalledBeforeTimeout() throws InterruptedException { + // GIVEN - a context with one pending message + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId = UUID.randomUUID(); + pendingMap.put(msgId, mockMsg); + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + var exception = new RuleEngineException("Test exception"); + + // WHEN - onFailure() is called in another thread before timeout + executorService.submit(() -> { + try { + Thread.sleep(100); + context.onFailure(tenantId, msgId, exception); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN - await() should return true (successful completion, even if message processing failed) + boolean result = context.await(5000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should return true when latch is counted down before timeout").isTrue(); + + // Verify the exception was added to exceptions map + assertThat(context.getSuccessMap()).isEmpty(); + assertThat(context.getPendingMap()).isEmpty(); + assertThat(context.getExceptionsMap()).containsEntry(tenantId, exception); + } + + @Test + void testAwait_shouldReturnFalse_whenTimeoutOccurs() throws InterruptedException { + // GIVEN - a context with one pending message and no processing + UUID msgId = UUID.randomUUID(); + pendingMap.put(msgId, mockMsg); + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + // WHEN - await() is called with short timeout and no message processing happens + long startTime = System.nanoTime(); + boolean result = context.await(100, TimeUnit.MILLISECONDS); + long elapsedTime = System.nanoTime() - startTime; + + // THEN - await() should return false (timeout occurred) + assertThat(result).as("await() should return false when timeout occurs").isFalse(); + assertThat(elapsedTime).as("await() should wait for at least the timeout duration").isGreaterThanOrEqualTo(100L); + + // Message should still be in pending map + assertThat(context.getSuccessMap()).isEmpty(); + assertThat(context.getPendingMap()).containsKey(msgId); + assertThat(context.getExceptionsMap()).isEmpty(); + } + + @Test + void testAwait_shouldHandleMultiplePendingMessages() throws InterruptedException { + // GIVEN - a context with multiple pending messages + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId1 = UUID.randomUUID(); + UUID msgId2 = UUID.randomUUID(); + UUID msgId3 = UUID.randomUUID(); + + pendingMap.put(msgId1, mockMsg); + pendingMap.put(msgId2, mockMsg); + pendingMap.put(msgId3, mockMsg); + + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + // WHEN - messages are processed one by one + executorService.submit(() -> { + try { + Thread.sleep(50); + context.onSuccess(msgId1); + Thread.sleep(50); + context.onSuccess(msgId2); + Thread.sleep(50); + context.onSuccess(msgId3); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN - await() should return true only after all messages are processed + boolean result = context.await(5000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should return true after all messages are processed").isTrue(); + + // All messages should be in success map + assertThat(context.getSuccessMap()).containsKeys(msgId1, msgId2, msgId3); + assertThat(context.getPendingMap()).isEmpty(); + assertThat(context.getExceptionsMap()).isEmpty(); + } + + @Test + void testAwait_shouldNotCountDownPrematurely_withMultipleMessages() throws InterruptedException { + // GIVEN - a context with multiple pending messages + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId1 = UUID.randomUUID(); + UUID msgId2 = UUID.randomUUID(); + + pendingMap.put(msgId1, mockMsg); + pendingMap.put(msgId2, mockMsg); + + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + // WHEN - only one message is processed + executorService.submit(() -> { + try { + Thread.sleep(100); + context.onSuccess(msgId1); + // msgId2 still in processing + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN: await should timeout because not all messages were processed + boolean result = context.await(2000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should timeout when not all messages are processed").isFalse(); + + // One message in success, one still pending + assertThat(context.getSuccessMap()).containsOnlyKeys(msgId1); + assertThat(context.getPendingMap()).containsOnlyKeys(msgId2); + assertThat(context.getExceptionsMap()).isEmpty(); + } + + @Test + void testAwait_shouldHandleMixedSuccessAndFailure() throws InterruptedException { + // GIVEN - multiple messages + executorService = Executors.newSingleThreadExecutor(); + + UUID msgId1 = UUID.randomUUID(); + UUID msgId2 = UUID.randomUUID(); + + pendingMap.put(msgId1, mockMsg); + pendingMap.put(msgId2, mockMsg); + + var context = new TbMsgPackProcessingContext("test-queue", submitStrategy, false); + + var exception = new RuleEngineException("Test exception"); + + // WHEN - one succeeds, one fails + executorService.submit(() -> { + try { + Thread.sleep(50); + context.onSuccess(msgId1); + Thread.sleep(50); + context.onFailure(tenantId, msgId2, exception); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + // THEN - await() should complete successfully + boolean result = context.await(5000, TimeUnit.MILLISECONDS); + assertThat(result).as("await() should return true when all messages are processed").isTrue(); + + assertThat(context.getSuccessMap()).containsOnlyKeys(msgId1); + assertThat(context.getPendingMap()).isEmpty(); + assertThat(context.getExceptionsMap()).containsEntry(tenantId, exception); + } + + @Test + void testHighConcurrencyCase() throws InterruptedException { int msgCount = 1000; int parallelCount = 5; executorService = Executors.newFixedThreadPool(parallelCount, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope")); @@ -76,28 +290,24 @@ public class TbMsgPackProcessingContextTest { final CountDownLatch startLatch = new CountDownLatch(1); final CountDownLatch finishLatch = new CountDownLatch(parallelCount); for (int i = 0; i < parallelCount; i++) { - //final String taskName = "" + uuid + " " + i; executorService.submit(() -> { - //log.warn("ready {}", taskName); readyLatch.countDown(); try { startLatch.await(); } catch (InterruptedException e) { - Assert.fail("failed to await"); + fail("failed to await"); } - //log.warn("go {}", taskName); - context.onSuccess(uuid); - finishLatch.countDown(); }); } - assertTrue(readyLatch.await(TIMEOUT, TimeUnit.SECONDS)); + assertTrue(readyLatch.await(10, TimeUnit.SECONDS)); Thread.yield(); startLatch.countDown(); //run all-at-once submitted tasks - assertTrue(finishLatch.await(TIMEOUT, TimeUnit.SECONDS)); + assertTrue(finishLatch.await(10, TimeUnit.SECONDS)); } - assertTrue(context.await(TIMEOUT, TimeUnit.SECONDS)); + assertTrue(context.await(10, TimeUnit.SECONDS)); verify(strategyMock, times(msgCount)).onSuccess(any(UUID.class)); } + } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/RuleEngineConsumerLoopTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/RuleEngineConsumerLoopTest.java new file mode 100644 index 0000000000..1619283586 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/RuleEngineConsumerLoopTest.java @@ -0,0 +1,281 @@ +/** + * 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.queue.ruleengine; + +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.actors.ActorSystemContext; +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.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.DefaultTbQueueMsgHeaders; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.memory.DefaultInMemoryStorage; +import org.thingsboard.server.queue.memory.InMemoryStorage; +import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory; +import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; +import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; +import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; +import org.thingsboard.server.service.stats.RuleEngineStatisticsService; + +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RuleEngineConsumerLoopTest { + + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + + InMemoryStorage storage; + + @Mock + ActorSystemContext actorContext; + @Mock + StatsFactory statsFactory; + @Mock + TbRuleEngineQueueFactory queueFactory; + @Mock + RuleEngineStatisticsService statisticsService; + @Mock + TbServiceInfoProvider serviceInfoProvider; + @Mock + PartitionService partitionService; + @Mock + TbQueueProducerProvider producerProvider; + @Mock + TbQueueAdmin queueAdmin; + @Mock + TbMsgPackProcessingContextFactory packProcessingContextFactory; + @Mock + TbMsgPackProcessingContext packCtx; + + Queue mainQueue; + + TbQueueConsumer> consumer; + + TbRuleEngineConsumerContext ruleEngineConsumerContext; + TbRuleEngineQueueConsumerManager consumerManager; + + ExecutorService consumersExecutor; + ScheduledExecutorService scheduler; + ExecutorService mgmtExecutor; + + @BeforeEach + void setup() throws InterruptedException { + consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer")); + scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("tb-rule-engine-consumer-scheduler"); + mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(1, "tb-rule-engine-mgmt"); + + mainQueue = new Queue(); + mainQueue.setTenantId(TenantId.SYS_TENANT_ID); + mainQueue.setName("Main"); + mainQueue.setTopic("tb_rule_engine.main"); + mainQueue.setPollInterval(25); + mainQueue.setPartitions(1); + mainQueue.setConsumerPerPartition(false); + mainQueue.setPackProcessingTimeout(2000L); + + var submitStrategy = new SubmitStrategy(); + submitStrategy.setType(SubmitStrategyType.BURST); + submitStrategy.setBatchSize(1000); + mainQueue.setSubmitStrategy(submitStrategy); + + var processingStrategy = new ProcessingStrategy(); + processingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES); + processingStrategy.setRetries(3); + processingStrategy.setFailurePercentage(0.0); + processingStrategy.setPauseBetweenRetries(3); + processingStrategy.setMaxPauseBetweenRetries(3); + mainQueue.setProcessingStrategy(processingStrategy); + + storage = new DefaultInMemoryStorage(); + + consumer = spy(new InMemoryTbQueueConsumer<>(storage, mainQueue.getTopic())); + given(queueFactory.createToRuleEngineMsgConsumer(eq(mainQueue), isNull())).willReturn(consumer); + + ruleEngineConsumerContext = new TbRuleEngineConsumerContext( + actorContext, statsFactory, new TbRuleEngineSubmitStrategyFactory(), new TbRuleEngineProcessingStrategyFactory(), + queueFactory, statisticsService, serviceInfoProvider, partitionService, producerProvider, queueAdmin + ); + ruleEngineConsumerContext.setPollDuration(25); + ruleEngineConsumerContext.setPackProcessingTimeout(2000); + ruleEngineConsumerContext.setStatsEnabled(false); // true by default + ruleEngineConsumerContext.setPrometheusStatsEnabled(false); + ruleEngineConsumerContext.setTopicDeletionDelayInSec(15); + ruleEngineConsumerContext.setMgmtThreadPoolSize(12); + + // Tell the (mock) context factory to return (mock) message pack context + given(packProcessingContextFactory.create( + eq(mainQueue.getName()), + any(TbRuleEngineSubmitStrategy.class), + eq(false) + )).willAnswer(invocation -> { + TbRuleEngineSubmitStrategy realStrategy = invocation.getArgument(1); + when(packCtx.getPendingMap()).thenAnswer(i -> realStrategy.getPendingMap()); + when(packCtx.getFailedMap()).thenReturn(new ConcurrentHashMap<>()); + return packCtx; + }); + + // Tell the (mock) context's await() to return 'false' (always timeout) immediately + given(packCtx.await(anyLong(), any(TimeUnit.class))).willReturn(false); + + consumerManager = TbRuleEngineQueueConsumerManager.create() + .ctx(ruleEngineConsumerContext) + .queueKey(new QueueKey(ServiceType.TB_RULE_ENGINE, mainQueue)) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .packProcessingContextFactory(packProcessingContextFactory) + .build(); + } + + @AfterEach + void destroy() { + MoreExecutors.shutdownAndAwaitTermination(scheduler, Duration.ofSeconds(30)); + MoreExecutors.shutdownAndAwaitTermination(mgmtExecutor, Duration.ofSeconds(30)); + MoreExecutors.shutdownAndAwaitTermination(consumersExecutor, Duration.ofSeconds(30)); + } + + @Test + void consumerLoopTest_verifyOperationsOrder() throws InterruptedException { + // Create partition + var partition = TopicPartitionInfo.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .topic(mainQueue.getTopic()) + .partition(0) + .myPartition(true) + .useInternalPartition(false) + .build(); + + // Put 10k messages to the queue + for (int i = 0; i < 10_000; i++) { + var tbMsg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(deviceId) + .data("{\"temperature\":123}") + .metaData(TbMsgMetaData.EMPTY) + .build(); + + var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder() + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTbMsgProto(TbMsg.toProto(tbMsg)) + .addAllRelationTypes(Set.of("Success")) + .build(); + + storage.put(partition.getFullTopicName(), new TbProtoQueueMsg<>(UUID.randomUUID(), toRuleEngineMsg, new DefaultTbQueueMsgHeaders())); + } + + // Count how many polls were made + var totalPolls = new AtomicInteger(0); + var emptyPolls = new AtomicInteger(0); + doAnswer(invocation -> { + totalPolls.incrementAndGet(); + @SuppressWarnings("unchecked") + var messages = (List) invocation.callRealMethod(); + if (messages.isEmpty()) { + emptyPolls.incrementAndGet(); + } + return messages; + }).when(consumer).poll(mainQueue.getPollInterval()); + + // Count how many commits were made + var totalCommits = new AtomicInteger(0); + doAnswer(invocation -> { + totalCommits.incrementAndGet(); + return invocation.callRealMethod(); + }).when(consumer).commit(); + + // Initialize consumer + consumerManager.init(mainQueue); + + // Assign partition to the consumer + consumerManager.update(Set.of(partition)); + + // Give some time for the consumer to get all messages + await().atMost(Duration.ofSeconds(10L)).until(() -> storage.getLagTotal() == 0); + + // Stop consumer + consumerManager.stop(); + consumerManager.awaitStop(); + + // Determine number of non-empty consumer iterations made, since polling does not stop immediately after consuming all messages and may do a few empty polls + int nonEmptyPolls = totalPolls.get() - emptyPolls.get(); + + // Verify that there is 10 polls and 10 matching commits + // Each poll consumes 1k messages and queue has 10k total, so that means 10k total msgs / 1k msgs per poll = 10 polls + assertThat(nonEmptyPolls).isEqualTo(10).isEqualTo(totalCommits.get()); + + // Verify that poll-await-commit cycle happened in order with correct await timeout + InOrder inOrder = inOrder(consumer, packCtx); + for (int i = 0; i < nonEmptyPolls; i++) { + inOrder.verify(consumer).poll(mainQueue.getPollInterval()); + inOrder.verify(packCtx).await(mainQueue.getPackProcessingTimeout(), TimeUnit.MILLISECONDS); + inOrder.verify(consumer).commit(); + } + } + +} From 56a83b6cfec7794a92cae6889722d0bb5e565d0d Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 28 Oct 2025 16:19:05 +0200 Subject: [PATCH 002/313] Fix existing test --- .../queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java | 2 ++ .../service/queue/ruleengine/TbRuleEngineStrategyTest.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index bcbe52b5c9..3c54d421e4 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -59,6 +59,7 @@ import org.thingsboard.server.queue.provider.KafkaMonolithQueueFactory; import org.thingsboard.server.queue.provider.KafkaTbRuleEngineQueueFactory; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; import org.thingsboard.server.service.stats.RuleEngineStatisticsService; @@ -194,6 +195,7 @@ public class TbRuleEngineQueueConsumerManagerTest { .consumerExecutor(consumersExecutor) .scheduler(scheduler) .taskExecutor(mgmtExecutor) + .packProcessingContextFactory(new TbMsgPackProcessingContextFactory.DefaultTbMsgPackProcessingContextFactory()) .build(); } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java index 1106fad5b6..c27a03619a 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java @@ -44,6 +44,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.service.queue.TbMsgPackProcessingContextFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; @@ -194,6 +195,7 @@ public class TbRuleEngineStrategyTest { var consumerManager = TbRuleEngineQueueConsumerManager.create() .ctx(ruleEngineConsumerContext) .queueKey(queueKey) + .packProcessingContextFactory(new TbMsgPackProcessingContextFactory.DefaultTbMsgPackProcessingContextFactory()) .build(); consumerManager.init(queue); From cb2bd954b6a99e32596efc9be84d16040d18fab8 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 3 Dec 2025 13:20:15 +0200 Subject: [PATCH 003/313] Fix default value for Prometheus rule engine stats --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2ee0d23502..2cb53a8a4b 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1913,7 +1913,7 @@ queue: max-error-message-length: "${TB_QUEUE_RULE_ENGINE_MAX_ERROR_MESSAGE_LENGTH:4096}" prometheus-stats: # Enable/disable Prometheus statistics for individual Rule Engine message processing (records time in ms for success/failure). - enabled: "${TB_QUEUE_RULE_ENGINE_PROMETHEUS_STATS_ENABLED:true}" + enabled: "${TB_QUEUE_RULE_ENGINE_PROMETHEUS_STATS_ENABLED:false}" # After a queue is deleted (or the profile's isolation option was disabled), Rule Engine will continue reading related topics during this period before deleting the actual topics topic-deletion-delay: "${TB_QUEUE_RULE_ENGINE_TOPIC_DELETION_DELAY_SEC:15}" # Size of the thread pool that handles such operations as partition changes, config updates, queue deletion From 3a098ef815bf55709ee8712ba3c1e3001a3e4c50 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 8 Dec 2025 16:56:35 +0200 Subject: [PATCH 004/313] REST API call node: better URL encoding # Conflicts: # common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java --- .../common/data/util/CollectionsUtil.java | 10 + rule-engine/rule-engine-components/pom.xml | 4 + .../rule/engine/rest/TbHttpClient.java | 58 +- .../rule/engine/rest/TbRestApiCallNode.java | 13 +- .../rest/TbRestApiCallNodeConfiguration.java | 26 + .../rule/engine/rest/TbHttpClientTest.java | 498 +++++++++++++++--- .../engine/rest/TbRestApiCallNodeTest.java | 114 ++++ 7 files changed, 641 insertions(+), 82 deletions(-) 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 082be9b71f..bdc4430387 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 @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.util; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -72,6 +73,15 @@ public class CollectionsUtil { return map; } + @SafeVarargs + public static LinkedHashMap orderedMapOf(Map.Entry... entries) { + LinkedHashMap map = new LinkedHashMap<>(); + for (Map.Entry entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + return map; + } + public static boolean emptyOrContains(Collection collection, V element) { return isEmpty(collection) || collection.contains(element); } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 90e6a87e52..45e37d03f9 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -161,6 +161,10 @@ org.thingsboard.langchain4j langchain4j + + org.apache.httpcomponents.core5 + httpcore5 + diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 63b4b70303..a16b3f5fb5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -15,12 +15,14 @@ */ package org.thingsboard.rule.engine.rest; +import com.google.common.collect.Maps; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.ssl.SslContext; import io.netty.handler.timeout.ReadTimeoutHandler; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.apache.hc.core5.net.URIBuilder; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -47,6 +49,7 @@ import reactor.netty.transport.ProxyProvider; import javax.net.ssl.SSLException; import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; @@ -213,8 +216,21 @@ public class TbHttpClient { } String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg); + + Map processedQueryParams; + if (config.isUseNewEncoding()) { + processedQueryParams = Maps.newHashMapWithExpectedSize(config.getQueryParams().size()); + config.getQueryParams().forEach((name, value) -> { + var processedParamName = TbNodeUtils.processPattern(name, msg); + var processedParamValue = TbNodeUtils.processPattern(value, msg); + processedQueryParams.put(processedParamName, processedParamValue); + }); + } else { + processedQueryParams = null; + } + HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); - URI uri = buildEncodedUri(endpointUrl); + URI uri = buildEncodedUri(endpointUrl, processedQueryParams); RequestBodySpec request = webClient .method(method) @@ -262,7 +278,7 @@ public class TbHttpClient { return origin; } - public URI buildEncodedUri(String endpointUrl) { + public URI buildEncodedUri(String endpointUrl, Map queryParams) { if (endpointUrl == null) { throw new RuntimeException("Url string cannot be null!"); } @@ -270,7 +286,13 @@ public class TbHttpClient { throw new RuntimeException("Url string cannot be empty!"); } - URI uri = UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri(); + URI uri; + if (queryParams != null) { + uri = buildEncodedUriNew(endpointUrl, queryParams); + } else { + uri = buildEncodedUriLegacy(endpointUrl); + } + if (uri.getScheme() == null || uri.getScheme().isEmpty()) { throw new RuntimeException("Transport scheme(protocol) must be provided!"); } @@ -284,6 +306,36 @@ public class TbHttpClient { return uri; } + private URI buildEncodedUriNew(String endpointUrl, Map queryParams) { + try { + URIBuilder builder = new URIBuilder(endpointUrl); + queryParams.forEach(builder::addParameter); + return builder.build(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(""" + Invalid REST endpoint URL: '%s'. The URL must be valid and properly encoded. + If the path contains special characters (e.g., spaces, braces), they must be percent-encoded (e.g., '/my file/' should be '/my%%20file/', '/path/{id}/' should be '/path/%%7Bid%%7D/'). + Query parameters should be configured separately and will be encoded automatically.""".formatted(endpointUrl), e); + } + } + + /** + * @deprecated Query params embedded in the URL string are not encoded correctly. + *

+ * In query strings, {@code +} is interpreted as a space (URL form encoding), + * so {@code email=user+tag@test.com} becomes {@code email=user tag@test.com} on the server. + *

+ * Pre-encoding the URL (e.g., {@code email=user%2Btag@test.com}) doesn't help + * because {@code .encode()} will double-encode it to {@code email=user%252Btag@test.com}. + *

+ * Use {@link #buildEncodedUriNew} with a separate query params map, + * where values are encoded exactly once: {@code +} → {@code %2B}, {@code @} → {@code %40}. + */ + @Deprecated(since = "4.2.1.2", forRemoval = true) // fixme: remove in major 5.0 + private URI buildEncodedUriLegacy(String endpointUrl) { + return UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri(); + } + private Object getData(TbMsg tbMsg, boolean parseToPlainText) { String data = tbMsg.getData(); return parseToPlainText ? JacksonUtil.toPlainText(data) : JacksonUtil.toJsonNode(data); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index a2290a4eda..3e9b034693 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -26,9 +26,12 @@ import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.dao.exception.DataValidationException; import java.util.List; +import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; + @RuleNode( type = ComponentType.EXTERNAL, name = "rest api call", @@ -57,7 +60,15 @@ public class TbRestApiCallNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx); - TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); + + var config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); + String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; + try { + validateFields(config, errorPrefix); + } catch (DataValidationException e) { + throw new TbNodeException(e, true); + } + httpClient = new TbHttpClient(config, ctx.getSharedEventLoop()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index 43ed0c597e..c896dea324 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -15,7 +15,10 @@ */ package org.thingsboard.rule.engine.rest; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -32,6 +35,8 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration queryParams; private Map headers; private boolean useSimpleClientHttpFactory; private int readTimeoutMs; @@ -48,12 +53,32 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration client.buildEncodedUri(url)); + assertThatThrownBy(() -> client.buildEncodedUri(url, null)); } @Test public void testBuildInvalidUri() { - Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod(); + Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); String url = "aaa"; - assertThatThrownBy(() -> client.buildEncodedUri(url)); + assertThatThrownBy(() -> client.buildEncodedUri(url, null)); } @Test public void testBuildUriWithSpecialSymbols() { - Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod(); + Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); String url = "http://192.168.1.1/data?d={\"a\": 12}"; String expected = "http://192.168.1.1/data?d=%7B%22a%22:%2012%7D"; - URI uri = client.buildEncodedUri(url); + URI uri = client.buildEncodedUri(url, null); Assertions.assertEquals(expected, uri.toString()); } + @Test + public void testProcessMessageWithQueryParamsPatternProcessing() throws Exception { + // GIVEN + String path = "/api/notify"; + try (var server = startClientAndServer("localhost", 0)) { + server.when( + request() + .withMethod("GET") + .withPath(path) + .withQueryStringParameter("email", "user+tag@test.com") + .withQueryStringParameter("device", "sensor-1") + .withQueryStringParameter("custom-header", "header-value") + .withQueryStringParameter("temp", "25.5") + .withQueryStringParameter("location", "room-1/zone-A") + ).respond( + response().withStatusCode(200) + ); + + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRestEndpointUrlPattern("http://localhost:" + server.getPort() + path); + config.setRequestMethod("GET"); + config.setUseSimpleClientHttpFactory(true); + config.setUseNewEncoding(true); + config.setQueryParams(Map.of( + "email", "${userEmail}", // ${} from metadata + "device", "${deviceName}", // ${} from metadata + "${dynamicParam}", "${dynamicValue}", // ${} in both key and value + "temp", "$[temperature]", // $[] from data + "location", "$[sensor.location]" // $[] from nested data + )); + + var metaData = new TbMsgMetaData(); + metaData.putValue("userEmail", "user+tag@test.com"); + metaData.putValue("deviceName", "sensor-1"); + metaData.putValue("dynamicParam", "custom-header"); + metaData.putValue("dynamicValue", "header-value"); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(new DeviceId(EntityId.NULL_UUID)) + .metaData(metaData) + .data(""" + {"temperature": 25.5, "sensor": {"location": "room-1/zone-A"}} + """) + .build(); + + // WHEN-THEN + processMessageAndWait(config, msg); + } + } + @Test public void testProcessMessageWithJsonInUrlVariable() throws Exception { - String host = "localhost"; + // GIVEN String path = "/api"; - String paramKey = "data"; - String paramVal = "[{\"test\":\"test\"}]"; - String successResponseBody = "SUCCESS"; - - var server = setUpDummyServer(host, path, paramKey, paramVal, successResponseBody); - - String endpointUrl = String.format( - "http://%s:%d%s?%s=%s", - host, server.getPort(), path, paramKey, paramVal - ); - String method = "GET"; - - - var config = new TbRestApiCallNodeConfiguration() - .defaultConfiguration(); - config.setRequestMethod(method); - config.setRestEndpointUrlPattern(endpointUrl); - config.setUseSimpleClientHttpFactory(true); + String paramValue = "[{\"test\":\"test\"}]"; + + try (var server = startClientAndServer("localhost", 0)) { + server.when( + request() + .withMethod("GET") + .withPath(path) + .withQueryStringParameter("data", paramValue) + ).respond( + response().withStatusCode(200) + ); + + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRestEndpointUrlPattern("http://localhost:" + server.getPort() + path + "?data=" + paramValue); + config.setRequestMethod("GET"); + config.setUseNewEncoding(false); + config.setQueryParams(null); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(new DeviceId(UUID.randomUUID())) + .metaData(new TbMsgMetaData()) + .data(TbMsg.EMPTY_JSON_OBJECT) + .build(); + + // WHEN-THEN + processMessageAndWait(config, msg); + } + } + private void processMessageAndWait(TbRestApiCallNodeConfiguration config, TbMsg msg) throws Exception { var httpClient = new TbHttpClient(config, eventLoop); - - var msg = TbMsg.newMsg() - .type(TbMsgType.POST_TELEMETRY_REQUEST) - .originator(new DeviceId(EntityId.NULL_UUID)) - .copyMetaData(TbMsgMetaData.EMPTY) - .data(TbMsg.EMPTY_JSON_OBJECT) - .build(); - var successMsg = TbMsg.newMsg() - .type(TbMsgType.POST_TELEMETRY_REQUEST) - .originator(msg.getOriginator()) - .copyMetaData(msg.getMetaData()) - .data(msg.getData()) - .build(); - var ctx = mock(TbContext.class); - when(ctx.transformMsg( - eq(msg), - eq(msg.getMetaData()), - eq(msg.getData()) - )).thenReturn(successMsg); - - var capturedData = ArgumentCaptor.forClass(String.class); + when(ctx.transformMsg(eq(msg), any(), any())).thenReturn(msg); - when(ctx.transformMsg( - eq(msg), - any(), - capturedData.capture() - )).thenReturn(successMsg); - - CountDownLatch latch = new CountDownLatch(1); + var latch = new CountDownLatch(1); + var error = new AtomicReference(); httpClient.processMessage(ctx, msg, m -> { - ctx.tellSuccess(msg); + ctx.tellSuccess(m); latch.countDown(); }, (m, t) -> { ctx.tellFailure(m, t); + error.set(t); latch.countDown(); }); - latch.await(5, TimeUnit.SECONDS); + assertTrue(latch.await(5, TimeUnit.SECONDS), "Request should complete within timeout"); + assertNull(error.get(), "Request should succeed, but got: " + error.get()); - verify(ctx, times(1)).tellSuccess(any()); - verify(ctx, times(0)).tellFailure(any(), any()); - Assertions.assertEquals(successResponseBody, capturedData.getValue()); + verify(ctx).tellSuccess(any()); + verify(ctx, never()).tellFailure(any(), any()); } - private ClientAndServer setUpDummyServer(String host, String path, String paramKey, String paramVal, String successResponseBody) { - var server = startClientAndServer(host, 1080); - createGetMethodExpectations(server, path, paramKey, paramVal, successResponseBody); - return server; + + @ParameterizedTest(name = "{0}") + @MethodSource + public void testQueryParamsEncoding(String endpointUrl, Map queryParams, String expectedEncodedUrl) { + // GIVEN + Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); + + // WHEN + URI uri = client.buildEncodedUri(endpointUrl, queryParams); + + // THEN + Assertions.assertEquals(expectedEncodedUrl, uri.toASCIIString()); } - private void createGetMethodExpectations(ClientAndServer server, String path, String paramKey, String paramVal, String successResponseBody) { - server.when( - request() - .withMethod("GET") - .withPath(path) - .withQueryStringParameter(paramKey, paramVal) - ).respond( - response() - .withStatusCode(200) - .withBody(successResponseBody) + private static Stream testQueryParamsEncoding() { + return Stream.of( + Arguments.of( + Named.named("ISO 8601 date-time in value", "http://somecompany/api/data/fetch"), + Map.of("ts", "2016-08-01T09:06:06.0+02:00"), + "http://somecompany/api/data/fetch?ts=2016-08-01T09%3A06%3A06.0%2B02%3A00" + ), + Arguments.of( + Named.named("email with plus sign in value", "http://localhost:8080/api/user/sendActivationMail"), + Map.of("email", "someperson+test1289@thingsboard.io"), + "http://localhost:8080/api/user/sendActivationMail?email=someperson%2Btest1289%40thingsboard.io" + ), + Arguments.of( + Named.named("plus mixed with spaces in value", "http://url/api"), + Map.of("q", "a + b"), + "http://url/api?q=a%20%2B%20b" + ), + Arguments.of( + Named.named("colon in value", "http://url/api"), + Map.of("time", "12:00"), + "http://url/api?time=12%3A00" + ), + Arguments.of( + Named.named("slash in value", "http://url"), + Map.of("ref", "/home/user"), + "http://url?ref=%2Fhome%2Fuser" + ), + Arguments.of( + Named.named("comma and semicolon in value", "http://url"), + Map.of("l", "a,b;c"), + "http://url?l=a%2Cb%3Bc" + ), + Arguments.of( + Named.named("ampersand and equals in value", "http://url"), + Map.of("q", "key1=value1&key2=value2"), + "http://url?q=key1%3Dvalue1%26key2%3Dvalue2" + ), + Arguments.of( + Named.named("JSON in value", "http://url"), + Map.of("json", """ + { + "string": "hello", + "integer": 42, + "float": 3.14, + "boolTrue": true, + "boolFalse": false, + "null": null, + "array": [ + 1, + "two", + true, + null + ], + "object": { + "nested": "value" + } + }"""), + "http://url?json=%7B%0A%20%20%20%20%22string%22%3A%20%22hello%22%2C%0A%20%20%20%20%22integer%22%3A%2042%2C%0A%20%20%20%20%22float%22%3A%203.14%2C%0A%20%20%20%20%22boolTrue%22%3A%20true%2C%0A%20%20%20%20%22boolFalse%22%3A%20false%2C%0A%20%20%20%20%22null%22%3A%20null%2C%0A%20%20%20%20%22array%22%3A%20%5B%0A%20%20%20%20%20%20%20%201%2C%0A%20%20%20%20%20%20%20%20%22two%22%2C%0A%20%20%20%20%20%20%20%20true%2C%0A%20%20%20%20%20%20%20%20null%0A%20%20%20%20%5D%2C%0A%20%20%20%20%22object%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22nested%22%3A%20%22value%22%0A%20%20%20%20%7D%0A%7D" + ), + Arguments.of( + Named.named("UTF-8 in query", "http://url/cafes"), + orderedMapOf( + entry("nom", "Le Goût Moderne"), + entry("назва", "У Миколи \uD83D\uDE0B") + ), + "http://url/cafes?nom=Le%20Go%C3%BBt%20Moderne&%D0%BD%D0%B0%D0%B7%D0%B2%D0%B0=%D0%A3%20%D0%9C%D0%B8%D0%BA%D0%BE%D0%BB%D0%B8%20%F0%9F%98%8B" + ), + Arguments.of( + Named.named("empty value", "http://url/empty"), + Map.of("name", ""), + "http://url/empty?name=" + ), + Arguments.of( + Named.named("blank value (spaces)", "http://url/empty"), + Map.of("name", " "), + "http://url/empty?name=%20%20" + ), + Arguments.of( + Named.named("empty key", "http://url/empty"), + Map.of("", "value"), + "http://url/empty?=value" + ), + Arguments.of( + Named.named("blank key (spaces)", "http://url/empty"), + Map.of(" ", "value"), + "http://url/empty?%20%20=value" + ), + Arguments.of( + Named.named("blank key with value that needs to be encoded", "http://url/empty"), + Map.of(" ", "value1+value2"), + "http://url/empty?%20%20=value1%2Bvalue2" + ), + Arguments.of( + Named.named("blank key and value", "http://url/empty"), + Map.of(" ", " "), + "http://url/empty?%20%20=%20%20" + ), + Arguments.of( + Named.named("fragment with query params", "http://url#frag"), + Map.of("docs", "مستندات عقدة القاعدة\n"), + "http://url?docs=%D9%85%D8%B3%D8%AA%D9%86%D8%AF%D8%A7%D8%AA%20%D8%B9%D9%82%D8%AF%D8%A9%20%D8%A7%D9%84%D9%82%D8%A7%D8%B9%D8%AF%D8%A9%0A#frag" + ), + Arguments.of( + Named.named("fragment only", "http://url#frag"), + Collections.emptyMap(), + "http://url#frag" + ), + Arguments.of( + Named.named("multiple query params (ordered)", "http://url/api"), + orderedMapOf( + entry("param1", "value1"), + entry("param2", "value2"), + entry("param3", "value3") + ), + "http://url/api?param1=value1¶m2=value2¶m3=value3" + ), + Arguments.of( + Named.named("hash (#) in value", "http://url/api"), + Map.of("color", "#ff0000"), + "http://url/api?color=%23ff0000" + ), + Arguments.of( + Named.named("question mark (?) in value", "http://url/api"), + Map.of("query", "what?"), + "http://url/api?query=what%3F" + ), + Arguments.of( + Named.named("percent sign (%) in value", "http://url/api"), + Map.of("discount", "50%"), + "http://url/api?discount=50%25" + ), + Arguments.of( + Named.named("already encoded string - double encoding", "http://url/api"), + Map.of("encoded", "%20"), + "http://url/api?encoded=%2520" + ), + Arguments.of( + Named.named("URL with port number", "http://localhost:8080/api/v1/data"), + Map.of("key", "value"), + "http://localhost:8080/api/v1/data?key=value" + ), + Arguments.of( + Named.named("URL with userinfo (auth)", "http://user:password@hostname/path"), + Map.of("secure", "true"), + "http://user:password@hostname/path?secure=true" + ), + Arguments.of( + Named.named("IPv4 address in URL", "http://192.168.1.100:9090/endpoint"), + Map.of("ip", "test"), + "http://192.168.1.100:9090/endpoint?ip=test" + ), + Arguments.of( + Named.named("IPv6 address in URL", "http://[::1]:8080/api"), + Map.of("ipv6", "true"), + "http://[::1]:8080/api?ipv6=true" + ), + Arguments.of( + Named.named("HTTPS protocol", "https://secure.example.com/api"), + Map.of("token", "abc123"), + "https://secure.example.com/api?token=abc123" + ), + Arguments.of( + Named.named("pipe (|) in value", "http://url/api"), + Map.of("filter", "a|b|c"), + "http://url/api?filter=a%7Cb%7Cc" + ), + Arguments.of( + Named.named("caret (^) and backtick (`) in value", "http://url/api"), + Map.of("special", "a^b`c"), + "http://url/api?special=a%5Eb%60c" + ), + Arguments.of( + Named.named("tab character in value", "http://url/api"), + Map.of("data", "col1\tcol2\tcol3"), + "http://url/api?data=col1%09col2%09col3" + ), + Arguments.of( + Named.named("CRLF in value", "http://url/api"), + Map.of("text", "line1\r\nline2"), + "http://url/api?text=line1%0D%0Aline2" + ), + Arguments.of( + Named.named("square brackets in value", "http://url/api"), + Map.of("array", "[1,2,3]"), + "http://url/api?array=%5B1%2C2%2C3%5D" + ), + Arguments.of( + Named.named("single and double quotes in value", "http://url/api"), + Map.of("quoted", "He said \"Hello\" and 'Hi'"), + "http://url/api?quoted=He%20said%20%22Hello%22%20and%20%27Hi%27" + ), + Arguments.of( + Named.named("angle brackets in value (XSS test)", "http://url/api"), + Map.of("html", ""), + "http://url/api?html=%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E" + ), + Arguments.of( + Named.named("backslash in value", "http://url/api"), + Map.of("path", "C:\\Users\\test"), + "http://url/api?path=C%3A%5CUsers%5Ctest" + ), + Arguments.of( + Named.named("at sign (@) in value", "http://url/api"), + Map.of("contact", "user@domain.com"), + "http://url/api?contact=user%40domain.com" + ), + Arguments.of( + Named.named("URL as value", "http://url/api"), + Map.of("redirect", "https://example.com/path?foo=bar"), + "http://url/api?redirect=https%3A%2F%2Fexample.com%2Fpath%3Ffoo%3Dbar" + ), + Arguments.of( + Named.named("empty map (not null)", "http://url/api"), + Collections.emptyMap(), + "http://url/api" + ), + Arguments.of( + Named.named("underscore and hyphen in key/value", "http://url/api"), + Map.of("my-key_name", "my-value_data"), + "http://url/api?my-key_name=my-value_data" + ), + Arguments.of( + Named.named("dots in key and value", "http://url/api"), + Map.of("version.major", "1.2.3"), + "http://url/api?version.major=1.2.3" + ), + Arguments.of( + Named.named("multiple reserved chars combined", "http://url/api"), + Map.of("complex", "a=1&b=2#section?query"), + "http://url/api?complex=a%3D1%26b%3D2%23section%3Fquery" + ), + Arguments.of( + Named.named("trailing slash in URL", "http://url/api/"), + Map.of("param", "value"), + "http://url/api/?param=value" + ), + Arguments.of( + Named.named("long value (1000 chars)", "http://url/api"), + Map.of("long", "a".repeat(1000)), + "http://url/api?long=" + "a".repeat(1000) + ), + Arguments.of( + Named.named("Chinese characters in value", "http://url/api"), + Map.of("greeting", "你好世界"), + "http://url/api?greeting=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C" + ), + Arguments.of( + Named.named("Japanese characters in value", "http://url/api"), + Map.of("text", "こんにちは"), + "http://url/api?text=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF" + ), + Arguments.of( + Named.named("existing query params in URL", "http://url/api?existing=param"), + Map.of("new", "value"), + "http://url/api?existing=param&new=value" + ), + Arguments.of( + Named.named("existing query and fragment in URL", "http://url/api?existing=param#section"), + Map.of("additional", "data"), + "http://url/api?existing=param&additional=data#section" + ), + Arguments.of( + Named.named("null byte in value", "http://url/api"), + Map.of("data", "before\u0000after"), + "http://url/api?data=before%00after" + ), + Arguments.of( + Named.named("form feed and vertical tab in value", "http://url/api"), + Map.of("whitespace", "a\fb\u000Bc"), + "http://url/api?whitespace=a%0Cb%0Bc" + ), + Arguments.of( + Named.named("tilde (unreserved, not encoded)", "http://url/api"), + Map.of("pattern", "~user"), + "http://url/api?pattern=~user" + ), + Arguments.of( + Named.named("asterisk in value", "http://url/api"), + Map.of("wildcard", "*.txt"), + "http://url/api?wildcard=%2A.txt" + ), + Arguments.of( + Named.named("curly braces in key", "http://url/api"), + Map.of("{key}", "value"), + "http://url/api?%7Bkey%7D=value" + ), + Arguments.of( + Named.named("curly braces in value", "http://url/api"), + Map.of("data", "{value}"), + "http://url/api?data=%7Bvalue%7D" + ), + Arguments.of( + Named.named("curly braces in both key and value", "http://url/api"), + Map.of("{param}", "{data}"), + "http://url/api?%7Bparam%7D=%7Bdata%7D" + ) ); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index 9c3121f790..8c3654bdc5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -25,6 +25,7 @@ import org.apache.http.impl.bootstrap.ServerBootstrap; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestHandler; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; @@ -43,24 +44,34 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import java.io.IOException; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { + private RuleNode ruleNode; + @Spy private TbRestApiCallNode restNode; @@ -94,6 +105,14 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { } } + @BeforeEach + public void setup() { + ruleNode = new RuleNode(); + ruleNode.setId(ruleNodeId); + ruleNode.setName("Test REST API call node"); + lenient().when(ctx.getSelf()).thenReturn(ruleNode); + } + @AfterEach public void teardown() { if (server != null) { @@ -101,6 +120,101 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { } } + @Test + public void shouldNotAllowNullQueryParamValues() { + // GIVEN + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setUseNewEncoding(true); + var params = new HashMap(); + params.put("key", null); + config.setQueryParams(params); + + // WHEN-THEN + assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .matches(e -> ((TbNodeException) e).isUnrecoverable()) + .rootCause() + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("query parameter values must be non-null"); + } + + @Test + public void shouldAllowOnlyNullQueryParamsIfOldEncodingIsUsed() { + // GIVEN + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setUseNewEncoding(false); + config.setQueryParams(Map.of("key", "value")); + + // WHEN-THEN + assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasRootCauseInstanceOf(DataValidationException.class) + .hasRootCauseMessage("'" + ruleNode.getName() + "' node configuration is invalid: query parameters must be null if old encoding is used") + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + @Test + public void shouldNotAllowNullQueryParamsIfNewEncodingIsUsed() { + // GIVEN + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setUseNewEncoding(true); + config.setQueryParams(null); + + // WHEN-THEN + assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasRootCauseInstanceOf(DataValidationException.class) + .hasRootCauseMessage("'" + ruleNode.getName() + "' node configuration is invalid: query parameters must be non-null if new encoding is used") + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + @Test + public void shouldUseNewEncodingForNewNodesByDefault() { + // GIVEN-WHEN + var defaultConfig = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + + // THEN + assertTrue(defaultConfig.isUseNewEncoding()); + assertEquals(Collections.emptyMap(), defaultConfig.getQueryParams()); + } + + @Test + public void shouldDefaultToOldEncodingForLegacyConfigs() { + // GIVEN + String configJson = """ + { + "restEndpointUrlPattern": "http://url?param=value", + "requestMethod": "GET", + "useSimpleClientHttpFactory": false, + "parseToPlainText": false, + "ignoreRequestBody": false, + "enableProxy": false, + "useSystemProxyProperties": false, + "proxyScheme": null, + "proxyHost": null, + "proxyPort": 0, + "proxyUser": null, + "proxyPassword": null, + "readTimeoutMs": 0, + "maxParallelRequestsCount": 0, + "headers": { + "Content-Type": "application/json", + "X-Authorization": "Bearer eyJhbGciOi...." + }, + "credentials": { + "type": "anonymous" + }, + "maxInMemoryBufferSizeInKb": 256 + }"""; + + // WHEN + var config = JacksonUtil.fromString(configJson, TbRestApiCallNodeConfiguration.class); + + // THEN + assertFalse(config.isUseNewEncoding()); + assertNull(config.getQueryParams()); + } + @Test public void deleteRequestWithoutBody() throws IOException, InterruptedException { final CountDownLatch latch = new CountDownLatch(1); From a36749b228a9bc044367bb087fd3d82d6f1bf8f8 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Tue, 16 Dec 2025 13:54:10 +0200 Subject: [PATCH 005/313] Refactor EdgeGrpcService, EdgeGrpcSession and related Postgres & Kafka event processing --- .../service/edge/EdgeContextComponent.java | 19 +- .../edge/rpc/DownlinkMessageMapper.java | 79 ++ .../service/edge/rpc/EdgeGrpcService.java | 711 ------------- .../service/edge/rpc/EdgeGrpcSession.java | 994 ------------------ .../service/edge/rpc/EdgeRpcService.java | 1 - .../service/edge/rpc/EdgeSessionState.java | 57 +- .../edge/rpc/EdgeUplinkMessageDispatcher.java | 223 ++++ .../server/service/edge/rpc/GrpcServer.java | 103 ++ .../edge/rpc/KafkaEdgeGrpcSession.java | 184 ---- .../edge/rpc/PostgresEdgeGrpcSession.java | 46 - .../rpc/processor/EdgeEventsDispatcher.java | 23 + .../PostgresGeneralEdgeEventsDispatcher.java | 218 ++++ .../edge/rpc/service/EdgeRpcServiceImpl.java | 386 +++++++ .../edge/rpc/session/BaseEdgeSession.java | 604 +++++++++++ .../DefaultZombieSessionCleanupService.java | 122 +++ .../session/EdgeAttributeSaveCallback.java | 49 + .../rpc/session/EdgeGrpcSessionDelegate.java | 34 + .../service/edge/rpc/session/EdgeSession.java | 43 + .../edge/rpc/session/EdgeSessionsHolder.java | 74 ++ .../session/ZombieSessionCleanupService.java | 23 + .../AbstractEdgeGrpcSessionManager.java | 128 +++ .../manager/EdgeGrpcSessionManager.java | 47 + .../KafkaBasedEdgeGrpcSessionManager.java | 282 +++++ .../PostgresBasedEdgeGrpcSessionManager.java | 180 ++++ .../src/test/resources/logback-test.xml | 3 - 25 files changed, 2690 insertions(+), 1943 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/PostgresEdgeGrpcSession.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeEventsDispatcher.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/PostgresGeneralEdgeEventsDispatcher.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeRpcServiceImpl.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/BaseEdgeSession.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeAttributeSaveCallback.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSessionDelegate.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSession.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/ZombieSessionCleanupService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/EdgeGrpcSessionManager.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.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..163758fa4b 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 @@ -15,11 +15,14 @@ */ package org.thingsboard.server.service.edge; +import jakarta.annotation.PreDestroy; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.edge.EdgeEventType; @@ -83,6 +86,8 @@ import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; @Lazy @Data @@ -91,8 +96,13 @@ import java.util.Optional; @TbCoreComponent public class EdgeContextComponent { + @Value("${edges.scheduler_pool_size}") + private int schedulerPoolSize; + private final Map processorMap = new EnumMap<>(EdgeEventType.class); + private ScheduledExecutorService edgeEventProcessingExecutorService; + @Autowired public EdgeContextComponent(List processors) { processors.forEach(processor -> { @@ -101,6 +111,14 @@ public class EdgeContextComponent { processorMap.put(eventType, processor); } }); + this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); + } + + @PreDestroy + private void destroy() { + if (edgeEventProcessingExecutorService != null && !edgeEventProcessingExecutorService.isShutdown()) { + edgeEventProcessingExecutorService.shutdownNow(); + } } // services @@ -206,7 +224,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/DownlinkMessageMapper.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java new file mode 100644 index 0000000000..3204d6901b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java @@ -0,0 +1,79 @@ +/** + * 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; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; + +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +@RequiredArgsConstructor +public class DownlinkMessageMapper { + + private final EdgeContextComponent ctx; + + public List convertToDownlinkMsgsPack(EdgeSessionState state, List edgeEvents) { + List result = new ArrayList<>(); + for (EdgeEvent edgeEvent : edgeEvents) { + log.trace("[{}][{}] converting edge event to downlink msg [{}]", state.getTenantId(), state.getEdgeId(), edgeEvent); + DownlinkMsg downlinkMsg = null; + try { + switch (edgeEvent.getAction()) { + case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, + ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, + ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { + downlinkMsg = convertEntityEventToDownlink(state, edgeEvent); + if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { + log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", + state.getTenantId(), state.getEdgeId(), downlinkMsg.getDownlinkMsgId()); + } else { + log.trace("[{}][{}] entity message processed [{}]", state.getTenantId(), state.getEdgeId(), downlinkMsg); + } + } + case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> + downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(state.getEdge(), edgeEvent); + default -> log.warn("[{}][{}] Unsupported action type [{}]", state.getTenantId(), state.getEdgeId(), edgeEvent.getAction()); + } + } catch (Exception e) { + log.trace("[{}][{}] Exception during converting edge event to downlink msg", state.getTenantId(), state.getEdgeId(), e); + } + if (downlinkMsg != null) { + result.add(downlinkMsg); + } + } + return result; + } + + protected DownlinkMsg convertEntityEventToDownlink(EdgeSessionState state, EdgeEvent edgeEvent) { + log.trace("[{}] Executing convertEntityEventToDownlink, edgeEvent [{}], action [{}]", edgeEvent.getTenantId(), edgeEvent, edgeEvent.getAction()); + if ((EdgeEventType.OAUTH2_CLIENT.equals(edgeEvent.getType()) || EdgeEventType.DOMAIN.equals(edgeEvent.getType())) && + (EdgeVersionUtils.isEdgeVersionOlderThan(state.getEdgeVersion(), EdgeVersion.V_3_8_0))) { + return null; + } + + return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent, state.getEdgeVersion()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java deleted file mode 100644 index 1827b986fd..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ /dev/null @@ -1,711 +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.service.edge.rpc; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import io.grpc.Server; -import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; -import io.grpc.stub.StreamObserver; -import jakarta.annotation.Nullable; -import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.rule.engine.api.AttributesSaveRequest; -import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; -import org.thingsboard.server.cache.TbTransactionalCache; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.ResourceUtils; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgDataType; -import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; -import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; -import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; -import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; -import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; -import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; -import org.thingsboard.server.gen.edge.v1.RequestMsg; -import org.thingsboard.server.gen.edge.v1.ResponseMsg; -import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; -import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.kafka.KafkaAdmin; -import org.thingsboard.server.queue.provider.TbCoreQueueFactory; -import org.thingsboard.server.queue.util.AfterStartUp; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.EdgeContextComponent; -import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Consumer; -import java.util.function.Function; - -import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE; -import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME; -import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME; - -@Service -@Slf4j -@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") -@TbCoreComponent -public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { - - private final ConcurrentMap sessions = new ConcurrentHashMap<>(); - private final ConcurrentMap sessionsById = new ConcurrentHashMap<>(); - private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); - private final Map sessionNewEvents = new HashMap<>(); - private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); - private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); - private final ConcurrentMap edgeEventsMigrationProcessed = new ConcurrentHashMap<>(); - private final List zombieSessions = new ArrayList<>(); - - @Value("${edges.rpc.port}") - private int rpcPort; - @Value("${edges.rpc.ssl.enabled}") - private boolean sslEnabled; - @Value("${edges.rpc.ssl.cert}") - private String certFileResource; - @Value("${edges.rpc.ssl.private_key}") - private String privateKeyResource; - @Value("${edges.state.persistToTelemetry:false}") - private boolean persistToTelemetry; - @Value("${edges.rpc.client_max_keep_alive_time_sec:1}") - private int clientMaxKeepAliveTimeSec; - @Value("${edges.rpc.max_inbound_message_size:4194304}") - private int maxInboundMessageSize; - @Value("${edges.rpc.keep_alive_time_sec:10}") - private int keepAliveTimeSec; - @Value("${edges.rpc.keep_alive_timeout_sec:5}") - private int keepAliveTimeoutSec; - @Value("${edges.scheduler_pool_size}") - private int schedulerPoolSize; - - @Value("${edges.send_scheduler_pool_size}") - private int sendSchedulerPoolSize; - - @Value("${edges.max_high_priority_queue_size_per_session:10000}") - private int maxHighPriorityQueueSizePerSession; - - @Autowired - @Lazy - private EdgeContextComponent ctx; - - @Autowired - private TelemetrySubscriptionService tsSubService; - - @Autowired - private TbClusterService clusterService; - - @Autowired - private TbServiceInfoProvider serviceInfoProvider; - - @Autowired - private TbTransactionalCache edgeIdServiceIdCache; - - @Autowired - private TopicService topicService; - - @Autowired - private TbCoreQueueFactory tbCoreQueueFactory; - - @Autowired - private Optional kafkaAdmin; - - private Server server; - - private ScheduledExecutorService edgeEventProcessingExecutorService; - - private ScheduledExecutorService sendDownlinkExecutorService; - - private ScheduledExecutorService executorService; - - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void onStartUp() { - log.info("Initializing Edge RPC service!"); - NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort) - .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) - .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS) - .keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS) - .permitKeepAliveWithoutCalls(true) - .maxInboundMessageSize(maxInboundMessageSize) - .addService(this); - if (sslEnabled) { - try { - InputStream certFileIs = ResourceUtils.getInputStream(this, certFileResource); - InputStream privateKeyFileIs = ResourceUtils.getInputStream(this, privateKeyResource); - builder.useTransportSecurity(certFileIs, privateKeyFileIs); - } catch (Exception e) { - log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); - throw new RuntimeException("Unable to set up SSL context!", e); - } - } - server = builder.build(); - log.info("Going to start Edge RPC server using port: {}", rpcPort); - try { - server.start(); - } catch (IOException e) { - log.error("Failed to start Edge RPC server!", e); - throw new RuntimeException("Failed to start Edge RPC server!"); - } - this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); - this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); - this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); - this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS); - log.info("Edge RPC service initialized!"); - } - - @PreDestroy - public void destroy() { - if (server != null) { - server.shutdownNow(); - } - for (Map.Entry> entry : sessionEdgeEventChecks.entrySet()) { - EdgeId edgeId = entry.getKey(); - ScheduledFuture sessionEdgeEventCheck = entry.getValue(); - if (sessionEdgeEventCheck != null && !sessionEdgeEventCheck.isCancelled() && !sessionEdgeEventCheck.isDone()) { - sessionEdgeEventCheck.cancel(true); - sessionEdgeEventChecks.remove(edgeId); - } - } - if (edgeEventProcessingExecutorService != null) { - edgeEventProcessingExecutorService.shutdownNow(); - } - if (sendDownlinkExecutorService != null) { - sendDownlinkExecutorService.shutdownNow(); - } - if (executorService != null) { - executorService.shutdownNow(); - } - } - - @Override - public StreamObserver handleMsgs(StreamObserver outputStream) { - EdgeGrpcSession session = createEdgeGrpcSession(outputStream); - return session.getInputStream(); - } - - private EdgeGrpcSession createEdgeGrpcSession(StreamObserver outputStream) { - return kafkaAdmin.isPresent() - ? new KafkaEdgeGrpcSession(ctx, topicService, tbCoreQueueFactory, kafkaAdmin.get(), outputStream, this::onEdgeConnect, this::onEdgeDisconnect, - sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession) - : new PostgresEdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, - sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); - } - - @Override - public void onToEdgeSessionMsg(TenantId tenantId, EdgeSessionMsg msg) { - switch (msg.getMsgType()) { - case EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG -> { - EdgeHighPriorityMsg edgeHighPriorityMsg = (EdgeHighPriorityMsg) msg; - log.trace("[{}] edgeEventMsg [{}]", tenantId, msg); - onEdgeHighPriorityEvent(edgeHighPriorityMsg); - } - case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG -> { - EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg; - log.trace("[{}] onToEdgeEventUpdateMsg [{}]", tenantId, msg); - onEdgeEventUpdate(tenantId, edgeEventUpdateMsg.getEdgeId()); - } - case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG -> { - ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg; - log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg); - startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId(), toEdgeSyncRequest.getServiceId()); - } - case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG -> { - FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg; - log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg); - processSyncResponse(fromEdgeSyncResponse); - } - } - } - - @Override - public void updateEdge(TenantId tenantId, Edge edge) { - if (edge == null) { - log.warn("[{}] Edge is null - edge is removed and outdated notification is in process!", tenantId); - return; - } - EdgeGrpcSession session = sessions.get(edge.getId()); - if (session != null && session.isConnected()) { - log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); - session.onConfigurationUpdate(edge); - } else { - log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); - } - } - - @Override - public void deleteEdge(TenantId tenantId, EdgeId edgeId) { - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null && session.isConnected()) { - log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); - destroySession(session); - session.cleanUp(); - sessions.remove(edgeId); - sessionsById.remove(session.getSessionId()); - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - sessionNewEvents.remove(edgeId); - } finally { - newEventLock.unlock(); - } - cancelScheduleEdgeEventsCheck(edgeId); - } - } - - private void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null && session.isConnected()) { - log.trace("[{}] onEdgeEventUpdate [{}]", tenantId, edgeId.getId()); - updateSessionEventsFlag(tenantId, edgeId); - } - } - - private void onEdgeHighPriorityEvent(EdgeHighPriorityMsg msg) { - TenantId tenantId = msg.getTenantId(); - EdgeEvent edgeEvent = msg.getEdgeEvent(); - EdgeId edgeId = edgeEvent.getEdgeId(); - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null && session.isConnected()) { - log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId); - session.addEventToHighPriorityQueue(edgeEvent); - updateSessionEventsFlag(tenantId, edgeId); - } - } - - private void updateSessionEventsFlag(TenantId tenantId, EdgeId edgeId) { - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) { - log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId()); - sessionNewEvents.put(edgeId, true); - } - } finally { - newEventLock.unlock(); - } - } - - private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) { - Edge edge = edgeGrpcSession.getEdge(); - TenantId tenantId = edge.getTenantId(); - log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId); - if (sessions.containsKey(edgeId)) { - EdgeGrpcSession existing = sessions.get(edgeId); - if (existing != null) { - log.info("[{}][{}] Replacing existing session [{}] for edge [{}]", tenantId, edgeGrpcSession.getSessionId(), existing.getSessionId(), edgeId); - destroySession(existing); - sessionsById.remove(existing.getSessionId()); - } - } - sessions.put(edgeId, edgeGrpcSession); - sessionsById.put(edgeGrpcSession.getSessionId(), edgeGrpcSession); - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - sessionNewEvents.put(edgeId, true); - } finally { - newEventLock.unlock(); - } - save(tenantId, edgeId, ACTIVITY_STATE, true); - long lastConnectTs = System.currentTimeMillis(); - save(tenantId, edgeId, LAST_CONNECT_TIME, lastConnectTs); - edgeIdServiceIdCache.put(edgeId, serviceInfoProvider.getServiceId()); - pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT); - cancelScheduleEdgeEventsCheck(edgeId); - edgeEventsMigrationProcessed.putIfAbsent(edgeId, Boolean.FALSE); - scheduleEdgeEventsCheck(edgeGrpcSession); - } - - private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId, String requestServiceId) { - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null) { - if (session.isSyncInProgress()) { - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"), requestServiceId); - } else { - boolean success = false; - if (session.isConnected()) { - session.startSyncProcess(true); - success = true; - } - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, ""), requestServiceId); - } - } - } - - @Override - public void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer) { - ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId, serviceInfoProvider.getServiceId()); - - UUID requestId = request.getId(); - EdgeGrpcSession session = sessions.get(request.getEdgeId()); - if (session != null && session.isSyncInProgress()) { - responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); - } else { - log.trace("[{}][{}] Processing sync edge request [{}], serviceId [{}]", request.getTenantId(), request.getId(), request.getEdgeId(), request.getServiceId()); - localSyncEdgeRequests.put(requestId, responseConsumer); - clusterService.pushEdgeSyncRequestToEdge(request); - scheduleSyncRequestTimeout(request, requestId); - } - } - - private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) { - log.trace("[{}] scheduling sync edge request", requestId); - executorService.schedule(() -> { - log.trace("[{}] checking if sync edge request is not processed...", requestId); - Consumer consumer = localSyncEdgeRequests.remove(requestId); - if (consumer != null) { - log.trace("[{}] timeout for processing sync edge request.", requestId); - consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Edge is not connected")); - } - }, 20, TimeUnit.SECONDS); - } - - private void processSyncResponse(FromEdgeSyncResponse response) { - log.trace("[{}] Received response from sync service: [{}]", response.getId(), response); - UUID requestId = response.getId(); - Consumer consumer = localSyncEdgeRequests.remove(requestId); - if (consumer != null) { - consumer.accept(response); - } else { - log.trace("[{}] Unknown or stale sync response received [{}]", requestId, response); - } - } - - private void scheduleEdgeEventsCheck(EdgeGrpcSession session) { - EdgeId edgeId = session.getEdge().getId(); - TenantId tenantId = session.getEdge().getTenantId(); - - cancelScheduleEdgeEventsCheck(edgeId); - - if (sessions.containsKey(edgeId)) { - ScheduledFuture edgeEventCheckTask = edgeEventProcessingExecutorService.schedule(() -> { - try { - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - if (Boolean.TRUE.equals(sessionNewEvents.get(edgeId))) { - log.trace("[{}][{}] set session new events flag to false", tenantId, edgeId.getId()); - sessionNewEvents.put(edgeId, false); - session.processHighPriorityEvents(); - processEdgeEventMigrationIfNeeded(session, edgeId); - if (Boolean.TRUE.equals(edgeEventsMigrationProcessed.get(edgeId))) { - Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() { - @Override - public void onSuccess(Boolean newEventsAdded) { - if (Boolean.TRUE.equals(newEventsAdded)) { - log.trace("[{}][{}] new events added. set session new events flag to true", tenantId, edgeId.getId()); - sessionNewEvents.put(edgeId, true); - } - scheduleEdgeEventsCheck(session); - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, session.getEdge().getId().getId(), t); - scheduleEdgeEventsCheck(session); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - scheduleEdgeEventsCheck(session); - } - } else { - scheduleEdgeEventsCheck(session); - } - } finally { - newEventLock.unlock(); - } - } catch (Exception e) { - log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, session.getEdge().getId().getId(), e); - } - }, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); - sessionEdgeEventChecks.put(edgeId, edgeEventCheckTask); - log.trace("[{}] Check edge event scheduled for edge [{}]", tenantId, edgeId.getId()); - } else { - log.debug("[{}] Session was removed and edge event check schedule must not be started [{}]", - tenantId, edgeId.getId()); - } - } - - private void processEdgeEventMigrationIfNeeded(EdgeGrpcSession session, EdgeId edgeId) throws Exception { - boolean isMigrationProcessed = edgeEventsMigrationProcessed.getOrDefault(edgeId, Boolean.FALSE); - if (!isMigrationProcessed) { - log.info("Starting edge event migration for edge [{}]", edgeId.getId()); - Boolean eventsExist = session.migrateEdgeEvents().get(); - if (Boolean.TRUE.equals(eventsExist)) { - log.info("Migration still in progress for edge [{}]", edgeId.getId()); - sessionNewEvents.put(edgeId, true); - scheduleEdgeEventsCheck(session); - } else if (Boolean.FALSE.equals(eventsExist)) { - log.info("Migration completed for edge [{}]", edgeId.getId()); - edgeEventsMigrationProcessed.put(edgeId, true); - } - } - } - - private void cancelScheduleEdgeEventsCheck(EdgeId edgeId) { - log.trace("[{}] cancelling edge event check for edge", edgeId); - if (sessionEdgeEventChecks.containsKey(edgeId)) { - ScheduledFuture sessionEdgeEventCheck = sessionEdgeEventChecks.get(edgeId); - if (sessionEdgeEventCheck != null && !sessionEdgeEventCheck.isCancelled() && !sessionEdgeEventCheck.isDone()) { - sessionEdgeEventCheck.cancel(true); - sessionEdgeEventChecks.remove(edgeId); - } - } - } - - private void onEdgeDisconnect(Edge edge, UUID sessionId) { - EdgeId edgeId = edge.getId(); - log.info("[{}][{}] edge disconnected!", edgeId, sessionId); - EdgeGrpcSession current = sessions.get(edgeId); - if (current != null && current.getSessionId().equals(sessionId)) { - EdgeGrpcSession toRemove = sessions.remove(edgeId); - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - sessionNewEvents.remove(edgeId); - } finally { - newEventLock.unlock(); - } - destroySession(toRemove); - sessionsById.remove(sessionId); - TenantId tenantId = toRemove.getEdge().getTenantId(); - save(tenantId, edgeId, ACTIVITY_STATE, false); - long lastDisconnectTs = System.currentTimeMillis(); - save(tenantId, edgeId, LAST_DISCONNECT_TIME, lastDisconnectTs); - pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); - cancelScheduleEdgeEventsCheck(edgeId); - } else { - log.info("[{}] edge session [{}] is not current anymore. Attempting to destroy it by sessionId.", edgeId, sessionId); - EdgeGrpcSession stale = sessionsById.remove(sessionId); - if (stale != null) { - try { - destroySession(stale); - log.info("[{}][{}] Successfully destroyed stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId); - } catch (Exception e) { - log.warn("[{}][{}] Failed to destroy stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId, e); - } - } else { - log.debug("[{}] No session found by sessionId [{}] to destroy", edgeId, sessionId); - } - } - edgeIdServiceIdCache.evict(edgeId); - } - - private void destroySession(EdgeGrpcSession session) { - try (session) { - if (!session.destroy()) { - log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", - session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId()); - zombieSessions.add(session); - } - } catch (Exception e) { - log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]", - session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId(), e); - } - } - - private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { - log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value); - if (persistToTelemetry) { - tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .entityId(edgeId) - .entry(new LongDataEntry(key, value)) - .callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) - .build()); - } else { - tsSubService.saveAttributes(AttributesSaveRequest.builder() - .tenantId(tenantId) - .entityId(edgeId) - .scope(AttributeScope.SERVER_SCOPE) - .entry(new LongDataEntry(key, value)) - .callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) - .build()); - } - } - - private void save(TenantId tenantId, EdgeId edgeId, String key, boolean value) { - log.debug("[{}][{}] Updating boolean edge telemetry [{}] [{}]", tenantId, edgeId, key, value); - if (persistToTelemetry) { - tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .entityId(edgeId) - .entry(new BooleanDataEntry(key, value)) - .callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) - .build()); - } else { - tsSubService.saveAttributes(AttributesSaveRequest.builder() - .tenantId(tenantId) - .entityId(edgeId) - .scope(AttributeScope.SERVER_SCOPE) - .entry(new BooleanDataEntry(key, value)) - .callback(new AttributeSaveCallback(tenantId, edgeId, key, value)) - .build()); - } - } - - private static class AttributeSaveCallback implements FutureCallback { - - private final TenantId tenantId; - private final EdgeId edgeId; - private final String key; - private final Object value; - - AttributeSaveCallback(TenantId tenantId, EdgeId edgeId, String key, Object value) { - this.tenantId = tenantId; - this.edgeId = edgeId; - this.key = key; - this.value = value; - } - - @Override - public void onSuccess(@Nullable Void result) { - log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value); - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t); - } - - } - - private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) { - try { - EdgeId edgeId = edge.getId(); - ObjectNode edgeState = JacksonUtil.newObjectNode(); - boolean isConnected = TbMsgType.CONNECT_EVENT.equals(msgType); - if (isConnected) { - edgeState.put(ACTIVITY_STATE, true); - edgeState.put(LAST_CONNECT_TIME, ts); - } else { - edgeState.put(ACTIVITY_STATE, false); - edgeState.put(LAST_DISCONNECT_TIME, ts); - } - ctx.getRuleProcessor().process(EdgeConnectionTrigger.builder() - .tenantId(tenantId) - .customerId(edge.getCustomerId()) - .edgeId(edgeId) - .edgeName(edge.getName()) - .connected(isConnected).build()); - String data = JacksonUtil.toString(edgeState); - TbMsgMetaData md = new TbMsgMetaData(); - if (!persistToTelemetry) { - md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); - md.putValue("edgeName", edge.getName()); - md.putValue("edgeType", edge.getType()); - } - TbMsg tbMsg = TbMsg.newMsg() - .type(msgType) - .originator(edgeId) - .copyMetaData(md) - .dataType(TbMsgDataType.JSON) - .data(data) - .build(); - clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); - } catch (Exception e) { - log.warn("[{}][{}] Failed to push {}", tenantId, edge.getId(), msgType, e); - } - } - - private void cleanupZombieSessions() { - try { - tryToDestroyZombieSessions(getZombieSessions(sessions.values()), s -> sessions.remove(s.getEdge().getId())); - tryToDestroyZombieSessions(getZombieSessions(sessionsById.values()), s -> sessionsById.remove(s.getSessionId())); - - zombieSessions.removeIf(zombie -> { - if (zombie.destroy()) { - log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].", - zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); - return true; - } else { - log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].", - zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); - return false; - } - }); - } catch (Exception e) { - log.warn("Failed to cleanup kafka sessions", e); - } - } - - private List getZombieSessions(Collection sessions) { - List result = new ArrayList<>(); - for (EdgeGrpcSession session : sessions) { - if (isKafkaSessionAndZombie(session)) { - result.add(session); - } - } - return result; - } - - private void tryToDestroyZombieSessions(List sessionsToRemove, Function removeFunc) { - for (EdgeGrpcSession toRemove : sessionsToRemove) { - log.info("[{}] Destroying session for edge because edge is not connected", toRemove.getEdge().getId()); - if (toRemove.destroy()) { - removeFunc.apply(toRemove); - } - } - } - - private boolean isKafkaSessionAndZombie(EdgeGrpcSession session) { - if (session instanceof KafkaEdgeGrpcSession kafkaSession) { - log.debug("[{}] kafkaSession.isConnected() = {}, kafkaSession.getConsumer().getConsumer().isStopped() = {}", - kafkaSession.getEdge().getId(), - kafkaSession.isConnected(), - kafkaSession.getConsumer() != null ? kafkaSession.getConsumer().getConsumer() != null ? kafkaSession.getConsumer().getConsumer().isStopped() : null : null); - return !kafkaSession.isConnected() && - kafkaSession.getConsumer() != null && - kafkaSession.getConsumer().getConsumer() != null && - !kafkaSession.getConsumer().getConsumer().isStopped(); - } - return false; - } - -} 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 deleted file mode 100644 index 14c1b5e30a..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ /dev/null @@ -1,994 +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.service.edge.rpc; - -import com.datastax.oss.driver.api.core.uuid.Uuids; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import io.grpc.stub.StreamObserver; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.springframework.data.util.Pair; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.AttributesSaveResult; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.common.data.limit.LimitedApi; -import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; -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.msg.edge.EdgeEventUpdateMsg; -import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; -import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; -import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; -import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; -import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; -import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; -import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; -import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; -import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; -import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; -import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg; -import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; -import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; -import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.DownlinkResponseMsg; -import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; -import org.thingsboard.server.gen.edge.v1.EdgeUpdateMsg; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.gen.edge.v1.EntityDataProto; -import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; -import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; -import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; -import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RequestMsg; -import org.thingsboard.server.gen.edge.v1.RequestMsgType; -import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; -import org.thingsboard.server.gen.edge.v1.ResponseMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; -import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; -import org.thingsboard.server.gen.edge.v1.UplinkMsg; -import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; -import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; -import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; -import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; -import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg; -import org.thingsboard.server.service.edge.EdgeContextComponent; -import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; -import org.thingsboard.server.service.edge.rpc.fetch.EdgeEventFetcher; -import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher; -import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; - -import java.io.Closeable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.BiConsumer; - -@Slf4j -@Data -public abstract class EdgeGrpcSession implements Closeable { - - private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; - private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId"; - - private static final int MAX_DOWNLINK_ATTEMPTS = 3; - private static final String RATE_LIMIT_REACHED = "Rate limit reached"; - - protected static final ConcurrentLinkedQueue highPriorityQueue = new ConcurrentLinkedQueue<>(); - - protected UUID sessionId; - private BiConsumer sessionOpenListener; - private BiConsumer sessionCloseListener; - - private final EdgeSessionState sessionState = new EdgeSessionState(); - private final ReentrantLock downlinkMsgLock = new ReentrantLock(); - private final Lock sequenceDependencyLock = new ReentrantLock(); - - protected EdgeContextComponent ctx; - protected Edge edge; - protected TenantId tenantId; - - private Long newStartTs; - private Long previousStartTs; - private Long newStartSeqId; - private Long previousStartSeqId; - - private StreamObserver inputStream; - private StreamObserver outputStream; - - private volatile boolean connected; - private volatile boolean syncInProgress; - - private EdgeVersion edgeVersion; - private int maxInboundMessageSize; - private int clientMaxInboundMessageSize; - private int maxHighPriorityQueueSizePerSession; - - private ScheduledExecutorService sendDownlinkExecutorService; - - public EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, - BiConsumer sessionOpenListener, - BiConsumer sessionCloseListener, - ScheduledExecutorService sendDownlinkExecutorService, - int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { - this.sessionId = UUID.randomUUID(); - this.ctx = ctx; - this.outputStream = outputStream; - this.sessionOpenListener = sessionOpenListener; - this.sessionCloseListener = sessionCloseListener; - this.sendDownlinkExecutorService = sendDownlinkExecutorService; - this.maxInboundMessageSize = maxInboundMessageSize; - this.maxHighPriorityQueueSizePerSession = maxHighPriorityQueueSizePerSession; - initInputStream(); - } - - protected abstract ListenableFuture migrateEdgeEvents() throws Exception; - - public void initInputStream() { - inputStream = new StreamObserver<>() { - @Override - public void onNext(RequestMsg requestMsg) { - if (!connected && requestMsg.getMsgType().equals(RequestMsgType.CONNECT_RPC_MESSAGE)) { - ConnectResponseMsg responseMsg = processConnect(requestMsg.getConnectRequestMsg()); - outputStream.onNext(ResponseMsg.newBuilder() - .setConnectResponseMsg(responseMsg) - .build()); - if (ConnectResponseCode.ACCEPTED != responseMsg.getResponseCode()) { - outputStream.onError(new RuntimeException(responseMsg.getErrorMsg())); - } else { - if (requestMsg.getConnectRequestMsg().hasMaxInboundMessageSize()) { - log.debug("[{}][{}] Client max inbound message size: {}", tenantId, sessionId, requestMsg.getConnectRequestMsg().getMaxInboundMessageSize()); - clientMaxInboundMessageSize = requestMsg.getConnectRequestMsg().getMaxInboundMessageSize(); - } - connected = true; - } - } - if (connected) { - if (requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) { - if (requestMsg.hasSyncRequestMsg()) { - boolean fullSync = false; - if (requestMsg.getSyncRequestMsg().hasFullSync()) { - fullSync = requestMsg.getSyncRequestMsg().getFullSync(); - } - startSyncProcess(fullSync); - } else { - syncInProgress = false; - } - } - if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE)) { - if (requestMsg.hasUplinkMsg()) { - onUplinkMsg(requestMsg.getUplinkMsg()); - } - if (requestMsg.hasDownlinkResponseMsg()) { - onDownlinkResponse(requestMsg.getDownlinkResponseMsg()); - } - } - } - } - - @Override - public void onError(Throwable t) { - log.trace("[{}][{}] Stream was terminated due to error:", tenantId, sessionId, t); - closeSession(); - } - - @Override - public void onCompleted() { - log.info("[{}][{}] Stream was closed and completed successfully!", tenantId, sessionId); - closeSession(); - } - - private void closeSession() { - connected = false; - if (edge != null) { - try { - sessionCloseListener.accept(edge, sessionId); - } catch (Exception ignored) { - } - } - try { - outputStream.onCompleted(); - } catch (Exception ignored) { - } - } - }; - } - - public void onConfigurationUpdate(Edge edge) { - log.debug("[{}] onConfigurationUpdate [{}]", sessionId, edge); - this.tenantId = edge.getTenantId(); - this.edge = edge; - EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() - .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)).build(); - ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder() - .setEdgeUpdateMsg(edgeConfig) - .build(); - sendDownlinkMsg(edgeConfigMsg); - } - - public void startSyncProcess(boolean fullSync) { - if (!syncInProgress) { - log.info("[{}][{}][{}] Staring edge sync process", tenantId, edge.getId(), sessionId); - syncInProgress = true; - interruptGeneralProcessingOnSync(); - doSync(new EdgeSyncCursor(ctx, edge, fullSync)); - } else { - log.info("[{}][{}][{}] Sync is already started, skipping starting it now", tenantId, edge.getId(), sessionId); - } - } - - private void doSync(EdgeSyncCursor cursor) { - if (cursor.hasNext()) { - EdgeEventFetcher next = cursor.getNext(); - log.debug("[{}][{}] starting sync process, cursor current idx = {}, class = {}", - tenantId, edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); - ListenableFuture> future = startProcessingEdgeEvents(next); - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable Pair result) { - doSync(cursor); - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Exception during sync process", tenantId, edge.getId(), t); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - log.info("[{}][{}] sync process completed", tenantId, edge.getId()); - DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) - .build(); - Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() { - @Override - public void onSuccess(Boolean isInterrupted) { - markSyncCompletedSendEdgeEventUpdate(); - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Exception during sending sync complete", tenantId, edge.getId(), t); - markSyncCompletedSendEdgeEventUpdate(); - } - }, ctx.getGrpcCallbackExecutorService()); - } - } - - protected void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) { - try { - log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); - processHighPriorityEvents(); - PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); - 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)); - } - log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); - List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); - Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { - @Override - public void onSuccess(@Nullable Boolean isInterrupted) { - if (Boolean.TRUE.equals(isInterrupted)) { - log.debug("[{}][{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId(), sessionId); - result.set(null); - } else { - if (isConnected() && pageData.hasNext()) { - processEdgeEvents(fetcher, pageLink.nextPageLink(), result); - } else { - EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1); - UUID idOffset = latestEdgeEvent.getUuidId(); - if (idOffset != null) { - Long newStartTs = Uuids.unixTimestamp(idOffset); - long newStartSeqId = latestEdgeEvent.getSeqId(); - result.set(Pair.of(newStartTs, newStartSeqId)); - } else { - result.set(null); - } - } - } - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}] Failed to send downlink msgs pack", edge.getId(), t); - result.setException(t); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); - result.set(null); - } - } catch (Exception e) { - log.error("[{}] Failed to fetch edge events", edge.getId(), e); - result.setException(e); - } - } - - private ConnectResponseMsg processConnect(ConnectRequestMsg request) { - log.trace("[{}] processConnect [{}]", sessionId, request); - Optional optional = ctx.getEdgeService().findEdgeByRoutingKey(TenantId.SYS_TENANT_ID, request.getEdgeRoutingKey()); - if (optional.isPresent()) { - edge = optional.get(); - tenantId = edge.getTenantId(); - try { - if (edge.getSecret().equals(request.getEdgeSecret())) { - sessionOpenListener.accept(edge.getId(), this); - edgeVersion = request.getEdgeVersion(); - processSaveEdgeVersionAsAttribute(request.getEdgeVersion().name()); - return ConnectResponseMsg.newBuilder() - .setResponseCode(ConnectResponseCode.ACCEPTED) - .setErrorMsg("") - .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)) - .setMaxInboundMessageSize(maxInboundMessageSize) - .build(); - } - String error = "Failed to validate the edge!"; - String failureMsg = String.format("%s Provided request secret: %s", error, request.getEdgeSecret()); - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) - .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); - return ConnectResponseMsg.newBuilder() - .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) - .setErrorMsg(failureMsg) - .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); - } catch (Exception e) { - String failureMsg = "Failed to process edge connection!"; - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) - .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); - log.error(failureMsg, e); - return ConnectResponseMsg.newBuilder() - .setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE) - .setErrorMsg(failureMsg) - .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); - } - } - return ConnectResponseMsg.newBuilder() - .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) - .setErrorMsg("Failed to find the edge! Routing key: " + request.getEdgeRoutingKey()) - .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); - } - - private void processSaveEdgeVersionAsAttribute(String edgeVersion) { - AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry(DataConstants.EDGE_VERSION_ATTR_KEY, edgeVersion), System.currentTimeMillis()); - ctx.getAttributesService().save(tenantId, edge.getId(), AttributeScope.SERVER_SCOPE, attributeKvEntry); - } - - private void interruptGeneralProcessingOnSync() { - log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", tenantId, edge.getId(), sessionId); - stopCurrentSendDownlinkMsgsTask(true); - } - - protected ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { - interruptPreviousSendDownlinkMsgsTask(); - - sessionState.setSendDownlinkMsgsFuture(SettableFuture.create()); - sessionState.getPendingMsgsMap().clear(); - - downlinkMsgsPack.forEach(msg -> sessionState.getPendingMsgsMap().put(msg.getDownlinkMsgId(), msg)); - scheduleDownlinkMsgsPackSend(1); - - return sessionState.getSendDownlinkMsgsFuture(); - } - - private void interruptPreviousSendDownlinkMsgsTask() { - if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone() - || sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) { - log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", tenantId, edge.getId(), sessionId); - stopCurrentSendDownlinkMsgsTask(true); - } else { - log.trace("[{}][{}][{}] Previous send downlink future is not active", tenantId, edge.getId(), sessionId); - } - } - - private void onUplinkMsg(UplinkMsg uplinkMsg) { - if (isRateLimitViolated(uplinkMsg)) { - return; - } - ListenableFuture> future = processUplinkMsg(uplinkMsg); - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable List result) { - sendResponseMessage(uplinkMsg.getUplinkMsgId(), true, null); - } - - @Override - public void onFailure(Throwable t) { - String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t); - sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg); - } - }, ctx.getGrpcCallbackExecutorService()); - } - - private boolean isRateLimitViolated(UplinkMsg uplinkMsg) { - if (!ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES, tenantId) || - !ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES_PER_EDGE, tenantId, edge.getId())) { - String errorMsg = String.format("Failed to process uplink message. %s", RATE_LIMIT_REACHED); - sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg); - return true; - } - return false; - } - - private void scheduleDownlinkMsgsPackSend(int attempt) { - Runnable sendDownlinkMsgsTask = () -> { - try { - if (!isConnected()) { - stopCurrentSendDownlinkMsgsTask(true); - return; - } - if (!sessionState.getPendingMsgsMap().values().isEmpty()) { - List copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); - if (attempt > 1) { - String error = "Failed to deliver the batch"; - String failureMsg = String.format("{%s} (size: {%s})", error, copy.size()); - if (attempt == 2) { - // Send a failure notification only on the second attempt. - // This ensures that failure alerts are sent just once to avoid redundant notifications. - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) - .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); - } - ctx.getStatsCounterService().ifPresent(statsCounterService -> - statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED, edge.getTenantId(), edge.getId(), 1)); - log.warn("[{}][{}] {} on attempt {}", tenantId, edge.getId(), failureMsg, attempt); - log.debug("[{}][{}] entities in failed batch: {}", tenantId, edge.getId(), copy); - } - log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size()); - for (DownlinkMsg downlinkMsg : copy) { - if (clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > clientMaxInboundMessageSize) { - String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + - "env variable on the edge and restart it.", clientMaxInboundMessageSize); - String message = String.format("Downlink msg size %s exceeds client max inbound message size %s. " + - "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it.", downlinkMsg.getSerializedSize(), clientMaxInboundMessageSize); - log.error("[{}][{}][{}] {} Message {}", tenantId, edge.getId(), sessionId, message, downlinkMsg); - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) - .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> - statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), 1)); - sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); - } else { - sendDownlinkMsg(ResponseMsg.newBuilder() - .setDownlinkMsg(downlinkMsg) - .build()); - } - } - if (attempt < MAX_DOWNLINK_ATTEMPTS) { - scheduleDownlinkMsgsPackSend(attempt + 1); - } else { - String failureMsg = String.format("Failed to deliver messages: %s", copy); - log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", - tenantId, edge.getId(), MAX_DOWNLINK_ATTEMPTS, copy); - 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())); - stopCurrentSendDownlinkMsgsTask(false); - } - } else { - stopCurrentSendDownlinkMsgsTask(false); - } - } catch (Exception e) { - log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", tenantId, edge.getId(), e.getMessage(), e); - stopCurrentSendDownlinkMsgsTask(true); - } - }; - - if (attempt == 1) { - sendDownlinkExecutorService.submit(sendDownlinkMsgsTask); - } else { - sessionState.setScheduledSendDownlinkTask( - sendDownlinkExecutorService.schedule( - sendDownlinkMsgsTask, - ctx.getEdgeEventStorageSettings().getSleepIntervalBetweenBatches(), - TimeUnit.MILLISECONDS) - ); - } - } - - private void sendResponseMessage(int uplinkMsgId, boolean success, String errorMsg) { - UplinkResponseMsg.Builder responseBuilder = UplinkResponseMsg.newBuilder() - .setUplinkMsgId(uplinkMsgId) - .setSuccess(success); - if (errorMsg != null) { - responseBuilder.setErrorMsg(errorMsg); - } - sendDownlinkMsg(ResponseMsg.newBuilder() - .setUplinkResponseMsg(responseBuilder.build()) - .build()); - } - - private void onDownlinkResponse(DownlinkResponseMsg msg) { - try { - if (msg.getSuccess()) { - sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); - 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()); - DownlinkMsg downlinkMsg = sessionState.getPendingMsgsMap().get(msg.getDownlinkMsgId()); - // if NOT timeseries or attributes failures - ack failed downlink - if (downlinkMsg.getEntityDataCount() == 0) { - sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); - } - } - if (sessionState.getPendingMsgsMap().isEmpty()) { - log.debug("[{}][{}][{}] Pending msgs map is empty. Stopping current iteration", tenantId, edge.getId(), sessionId); - stopCurrentSendDownlinkMsgsTask(false); - } - } catch (Exception e) { - log.error("[{}][{}] Can't process downlink response message [{}]", tenantId, edge.getId(), msg, e); - } - } - - public void processHighPriorityEvents() { - try { - if (isConnected() && !isSyncInProgress()) { - if (highPriorityQueue.isEmpty()) { - return; - } - List highPriorityEvents = new ArrayList<>(); - EdgeEvent event; - while ((event = highPriorityQueue.poll()) != null) { - highPriorityEvents.add(event); - } - log.trace("[{}][{}] Sending high priority events {}", tenantId, edge.getId(), highPriorityEvents.size()); - List downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents); - sendDownlinkMsgsPack(downlinkMsgsPack).get(); - } - } catch (Exception e) { - log.error("[{}] Failed to process high priority events", edge.getId(), e); - } - } - - public ListenableFuture processEdgeEvents() throws Exception { - SettableFuture result = SettableFuture.create(); - if (isConnected() && !isSyncInProgress()) { - Pair startTsAndSeqId = getQueueStartTsAndSeqId().get(); - previousStartTs = startTsAndSeqId.getFirst(); - previousStartSeqId = startTsAndSeqId.getSecond(); - GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher( - previousStartTs, - previousStartSeqId, - false, - Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), - ctx.getEdgeEventService(), - ctx.getEdgeEventStorageSettings().getMisorderingCompensationMillis()); - log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", - tenantId, edge.getId(), previousStartTs, previousStartSeqId); - Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { - @Override - public void onSuccess(@Nullable Pair newStartTsAndSeqId) { - if (newStartTsAndSeqId != null) { - ListenableFuture updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId); - Futures.addCallback(updateFuture, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable AttributesSaveResult saveResult) { - log.debug("[{}][{}] queue offset was updated [{}]", tenantId, edge.getId(), newStartTsAndSeqId); - boolean newEventsAvailable; - if (fetcher.isSeqIdNewCycleStarted()) { - newEventsAvailable = isNewEdgeEventsAvailable(); - } else { - newEventsAvailable = isSeqIdStartedNewCycle(); - if (!newEventsAvailable) { - newEventsAvailable = isNewEdgeEventsAvailable(); - } - } - result.set(newEventsAvailable); - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, edge.getId(), newStartTsAndSeqId, t); - result.setException(t); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, edge.getId()); - result.set(Boolean.FALSE); - } - } - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to process events", tenantId, edge.getId(), t); - result.setException(t); - } - }, ctx.getGrpcCallbackExecutorService()); - } else { - if (isSyncInProgress()) { - log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, edge.getId()); - result.set(Boolean.TRUE); - } else { - log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, edge.getId()); - result.set(null); - } - } - return result; - } - - protected List convertToDownlinkMsgsPack(List edgeEvents) { - List result = new ArrayList<>(); - for (EdgeEvent edgeEvent : edgeEvents) { - log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, edge.getId(), edgeEvent); - DownlinkMsg downlinkMsg = null; - try { - switch (edgeEvent.getAction()) { - case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, - ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, - ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { - downlinkMsg = convertEntityEventToDownlink(edgeEvent); - if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { - log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", tenantId, edge.getId(), downlinkMsg.getDownlinkMsgId()); - } else { - log.trace("[{}][{}] entity message processed [{}]", tenantId, edge.getId(), downlinkMsg); - } - } - case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> - downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); - default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, edge.getId(), edgeEvent.getAction()); - } - } catch (Exception e) { - log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, edge.getId(), e); - } - if (downlinkMsg != null) { - result.add(downlinkMsg); - } - } - return result; - } - - private ListenableFuture> getQueueStartTsAndSeqId() { - ListenableFuture> future = - ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), AttributeScope.SERVER_SCOPE, Arrays.asList(QUEUE_START_TS_ATTR_KEY, QUEUE_START_SEQ_ID_ATTR_KEY)); - return Futures.transform(future, attributeKvEntries -> { - long startTs = 0L; - long startSeqId = 0L; - for (AttributeKvEntry attributeKvEntry : attributeKvEntries) { - if (QUEUE_START_TS_ATTR_KEY.equals(attributeKvEntry.getKey())) { - startTs = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; - } - if (QUEUE_START_SEQ_ID_ATTR_KEY.equals(attributeKvEntry.getKey())) { - startSeqId = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; - } - } - if (startSeqId == 0L) { - startSeqId = findStartSeqIdFromOldestEventIfAny(); - } - return Pair.of(startTs, startSeqId); - }, ctx.getGrpcCallbackExecutorService()); - } - - private boolean isSeqIdStartedNewCycle() { - try { - log.trace("[{}][{}][{}] Checking if seq id started new cycle", tenantId, edge.getId(), sessionId); - TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); - PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, previousStartSeqId == 0 ? null : previousStartSeqId - 1, pageLink); - boolean result = !edgeEvents.getData().isEmpty(); - log.trace("[{}][{}][{}] Result of check if seq id started new cycle, result = {}", tenantId, edge.getId(), sessionId, result); - return result; - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", tenantId, edge.getId(), sessionId, e); - } - return false; - } - - private boolean isNewEdgeEventsAvailable() { - try { - log.trace("[{}][{}][{}] Checking if new edge events available", tenantId, edge.getId(), sessionId); - TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); - PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), newStartSeqId, null, pageLink); - boolean result = !edgeEvents.getData().isEmpty() || !highPriorityQueue.isEmpty(); - log.trace("[{}][{}][{}] Result of check if new edge events available, result = {}", tenantId, edge.getId(), sessionId, result); - return result; - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", tenantId, edge.getId(), sessionId, e); - } - return false; - } - - private long findStartSeqIdFromOldestEventIfAny() { - long startSeqId = 0L; - try { - TimePageLink pageLink = new TimePageLink(1, 0, null, null, null, null); - PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), null, null, pageLink); - if (!edgeEvents.getData().isEmpty()) { - startSeqId = edgeEvents.getData().get(0).getSeqId() - 1; - } - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", tenantId, edge.getId(), sessionId, e); - } - return startSeqId; - } - - private ListenableFuture updateQueueStartTsAndSeqId(Pair pair) { - newStartTs = pair.getFirst(); - newStartSeqId = pair.getSecond(); - log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", sessionId, edge.getId(), newStartTs, newStartSeqId); - List attributes = List.of( - new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()), - new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, newStartSeqId), System.currentTimeMillis()) - ); - return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), AttributeScope.SERVER_SCOPE, attributes); - } - - protected ListenableFuture> startProcessingEdgeEvents(EdgeEventFetcher fetcher) { - SettableFuture> result = SettableFuture.create(); - PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()); - processEdgeEvents(fetcher, pageLink, result); - return result; - } - - private void markSyncCompletedSendEdgeEventUpdate() { - syncInProgress = false; - ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId())); - } - - private void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) { - if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) { - sessionState.getSendDownlinkMsgsFuture().set(isInterrupted); - } - if (sessionState.getScheduledSendDownlinkTask() != null) { - sessionState.getScheduledSendDownlinkTask().cancel(true); - } - } - - private void sendDownlinkMsg(ResponseMsg responseMsg) { - if (isConnected()) { - String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000); - log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, edge.getId(), responseMsgStr); - downlinkMsgLock.lock(); - String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr; - try { - outputStream.onNext(responseMsg); - } catch (Exception e) { - log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, edge.getId(), downlinkMsgStr, e); - connected = false; - sessionCloseListener.accept(edge, sessionId); - } finally { - downlinkMsgLock.unlock(); - } - log.trace("[{}][{}] downlink msg successfully sent [{}]", tenantId, edge.getId(), downlinkMsgStr); - } - } - - protected DownlinkMsg convertEntityEventToDownlink(EdgeEvent edgeEvent) { - log.trace("[{}] Executing convertEntityEventToDownlink, edgeEvent [{}], action [{}]", edgeEvent.getTenantId(), edgeEvent, edgeEvent.getAction()); - if ((EdgeEventType.OAUTH2_CLIENT.equals(edgeEvent.getType()) || EdgeEventType.DOMAIN.equals(edgeEvent.getType())) && - (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0))) { - return null; - } - - return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent, edgeVersion); - } - - public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { - while (highPriorityQueue.size() > maxHighPriorityQueueSizePerSession) { - EdgeEvent oldestHighPriority = highPriorityQueue.poll(); - if (oldestHighPriority != null) { - log.warn("[{}][{}][{}] High priority queue is full. Removing oldest high priority event from queue {}", - tenantId, edge.getId(), sessionId, oldestHighPriority); - } - } - highPriorityQueue.add(edgeEvent); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1)); - } - - protected ListenableFuture> processUplinkMsg(UplinkMsg uplinkMsg) { - List> result = new ArrayList<>(); - try { - if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) { - for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) { - result.add(ctx.getDeviceProfileProcessor().processDeviceProfileMsgFromEdge(edge.getTenantId(), edge, deviceProfileUpdateMsg)); - } - } - if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { - for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg)); - } - } - if (uplinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { - for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg : uplinkMsg.getDeviceCredentialsUpdateMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsgFromEdge(edge.getTenantId(), edge.getId(), deviceCredentialsUpdateMsg)); - } - } - if (uplinkMsg.getAssetProfileUpdateMsgCount() > 0) { - for (AssetProfileUpdateMsg assetProfileUpdateMsg : uplinkMsg.getAssetProfileUpdateMsgList()) { - result.add(ctx.getAssetProfileProcessor().processAssetProfileMsgFromEdge(edge.getTenantId(), edge, assetProfileUpdateMsg)); - } - } - if (uplinkMsg.getAssetUpdateMsgCount() > 0) { - for (AssetUpdateMsg assetUpdateMsg : uplinkMsg.getAssetUpdateMsgList()) { - result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); - } - } - if (uplinkMsg.getRuleChainUpdateMsgCount() > 0) { - for (RuleChainUpdateMsg ruleChainUpdateMsg : uplinkMsg.getRuleChainUpdateMsgList()) { - result.add(ctx.getRuleChainProcessor().processRuleChainMsgFromEdge(edge.getTenantId(), edge, ruleChainUpdateMsg)); - } - } - if (uplinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) { - for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg : uplinkMsg.getRuleChainMetadataUpdateMsgList()) { - result.add(ctx.getRuleChainProcessor().processRuleChainMetadataMsgFromEdge(edge.getTenantId(), edge, ruleChainMetadataUpdateMsg)); - } - } - if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) { - for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) { - result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg)); - } - } - if (uplinkMsg.getEntityDataCount() > 0) { - for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) { - result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData)); - } - } - if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { - for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { - result.add(ctx.getAlarmProcessor().processAlarmMsgFromEdge(edge.getTenantId(), edge.getId(), alarmUpdateMsg)); - } - } - if (uplinkMsg.getAlarmCommentUpdateMsgCount() > 0) { - for (AlarmCommentUpdateMsg alarmCommentUpdateMsg : uplinkMsg.getAlarmCommentUpdateMsgList()) { - result.add(ctx.getAlarmCommentProcessor().processAlarmCommentMsgFromEdge(edge.getTenantId(), edge.getId(), alarmCommentUpdateMsg)); - } - } - if (uplinkMsg.getRelationUpdateMsgCount() > 0) { - for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { - result.add(ctx.getRelationProcessor().processRelationMsgFromEdge(edge.getTenantId(), edge, relationUpdateMsg)); - } - } - if (uplinkMsg.getDashboardUpdateMsgCount() > 0) { - for (DashboardUpdateMsg dashboardUpdateMsg : uplinkMsg.getDashboardUpdateMsgList()) { - result.add(ctx.getDashboardProcessor().processDashboardMsgFromEdge(edge.getTenantId(), edge, dashboardUpdateMsg)); - } - } - if (uplinkMsg.getResourceUpdateMsgCount() > 0) { - for (ResourceUpdateMsg resourceUpdateMsg : uplinkMsg.getResourceUpdateMsgList()) { - result.add(ctx.getResourceProcessor().processResourceMsgFromEdge(edge.getTenantId(), edge, resourceUpdateMsg)); - } - } - if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { - for (RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg : uplinkMsg.getRuleChainMetadataRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processRuleChainMetadataRequestMsg(edge.getTenantId(), edge, ruleChainMetadataRequestMsg)); - } - } - if (uplinkMsg.getAttributesRequestMsgCount() > 0) { - for (AttributesRequestMsg attributesRequestMsg : uplinkMsg.getAttributesRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processAttributesRequestMsg(edge.getTenantId(), edge, attributesRequestMsg)); - } - } - if (uplinkMsg.getRelationRequestMsgCount() > 0) { - for (RelationRequestMsg relationRequestMsg : uplinkMsg.getRelationRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg)); - } - } - if (uplinkMsg.getCalculatedFieldRequestMsgCount() > 0) { - for (CalculatedFieldRequestMsg calculatedFieldRequestMsg : uplinkMsg.getCalculatedFieldRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processCalculatedFieldRequestMsg(edge.getTenantId(), edge, calculatedFieldRequestMsg)); - } - } - if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) { - for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg)); - } - } - if (uplinkMsg.getDeviceCredentialsRequestMsgCount() > 0) { - for (DeviceCredentialsRequestMsg deviceCredentialsRequestMsg : uplinkMsg.getDeviceCredentialsRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processDeviceCredentialsRequestMsg(edge.getTenantId(), edge, deviceCredentialsRequestMsg)); - } - } - if (uplinkMsg.getDeviceRpcCallMsgCount() > 0) { - for (DeviceRpcCallMsg deviceRpcCallMsg : uplinkMsg.getDeviceRpcCallMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceRpcCallFromEdge(edge.getTenantId(), edge, deviceRpcCallMsg)); - } - } - if (uplinkMsg.getWidgetBundleTypesRequestMsgCount() > 0) { - for (WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg : uplinkMsg.getWidgetBundleTypesRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processWidgetBundleTypesRequestMsg(edge.getTenantId(), edge, widgetBundleTypesRequestMsg)); - } - } - if (uplinkMsg.getEntityViewsRequestMsgCount() > 0) { - for (EntityViewsRequestMsg entityViewRequestMsg : uplinkMsg.getEntityViewsRequestMsgList()) { - result.add(ctx.getEdgeRequestsService().processEntityViewsRequestMsg(edge.getTenantId(), edge, entityViewRequestMsg)); - } - } - if (uplinkMsg.getCalculatedFieldUpdateMsgCount() > 0) { - for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : uplinkMsg.getCalculatedFieldUpdateMsgList()) { - result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); - } - } - if (uplinkMsg.getAiModelUpdateMsgCount() > 0) { - for (AiModelUpdateMsg aiModelUpdateMsg : uplinkMsg.getAiModelUpdateMsgList()) { - result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg)); - } - } - if (uplinkMsg.getUserUpdateMsgCount() > 0) { - for (UserUpdateMsg userUpdateMsg : uplinkMsg.getUserUpdateMsgList()) { - sequenceDependencyLock.lock(); - try { - result.add(ctx.getUserProcessor().processUserMsgFromEdge(edge.getTenantId(), edge, userUpdateMsg)); - } finally { - sequenceDependencyLock.unlock(); - } - } - } - if (uplinkMsg.getUserCredentialsUpdateMsgCount() > 0) { - for (UserCredentialsUpdateMsg userCredentialsUpdateMsg : uplinkMsg.getUserCredentialsUpdateMsgList()) { - sequenceDependencyLock.lock(); - try { - result.add(ctx.getUserProcessor().processUserCredentialsMsgFromEdge(edge.getTenantId(), edge, userCredentialsUpdateMsg)); - } finally { - sequenceDependencyLock.unlock(); - } - } - } - } catch (Exception e) { - String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); - log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) - .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); - return Futures.immediateFailedFuture(e); - } - return Futures.allAsList(result); - } - - protected boolean destroy() { - return true; - } - - protected void cleanUp() {} - - @Override - public void close() { - log.debug("[{}][{}] Closing session", tenantId, sessionId); - connected = false; - try { - outputStream.onCompleted(); - } catch (Exception e) { - log.debug("[{}][{}] Failed to close output stream: {}", tenantId, sessionId, e.getMessage()); - } - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java index 3907dfeb7f..aae6c7ad75 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java @@ -20,7 +20,6 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; -import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import java.util.function.Consumer; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java index f9071183d6..bc7883feae 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java @@ -16,19 +16,70 @@ package org.thingsboard.server.service.edge.rpc; import com.google.common.util.concurrent.SettableFuture; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; -@Data +@Getter public class EdgeSessionState { - private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); + @Setter private SettableFuture sendDownlinkMsgsFuture; + @Setter private ScheduledFuture scheduledSendDownlinkTask; + @Setter + private volatile boolean connected; + @Setter + private EdgeVersion edgeVersion; + private final UUID sessionId = UUID.randomUUID(); + private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); + private final Lock sequenceDependencyLock = new ReentrantLock(); + private final AtomicBoolean syncInProgress = new AtomicBoolean(false); + private TenantId tenantId; + private Edge edge; + + public void setEdge(Edge edge) { + if (edge == null) { + return; + } + this.edge = edge; + this.tenantId = edge.getTenantId(); + } + + public EdgeId getEdgeId() { + return edge != null ? edge.getId() : null; + } + + public boolean tryStartSync() { + return syncInProgress.compareAndSet(false, true); + } + + public void finishSync() { + syncInProgress.set(false); + } + + public boolean isSyncInProgress() { + return syncInProgress.get(); + } + + public void lockSequenceDependency() { + sequenceDependencyLock.lock(); + } + public void unlockSequenceDependency() { + sequenceDependencyLock.unlock(); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java new file mode 100644 index 0000000000..83628396df --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java @@ -0,0 +1,223 @@ +/** + * 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; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EntityDataProto; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; +import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; +import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg; +import org.thingsboard.server.service.edge.EdgeContextComponent; + +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class EdgeUplinkMessageDispatcher { + + private final EdgeContextComponent ctx; + + public ListenableFuture> processUplinkMsg(EdgeSessionState state, UplinkMsg uplinkMsg) { + Edge edge = state.getEdge(); + List> result = new ArrayList<>(); + try { + if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) { + for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) { + result.add(ctx.getDeviceProfileProcessor().processDeviceProfileMsgFromEdge(edge.getTenantId(), edge, deviceProfileUpdateMsg)); + } + } + if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { + for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { + result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg)); + } + } + if (uplinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { + for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg : uplinkMsg.getDeviceCredentialsUpdateMsgList()) { + result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsgFromEdge(edge.getTenantId(), edge.getId(), deviceCredentialsUpdateMsg)); + } + } + if (uplinkMsg.getAssetProfileUpdateMsgCount() > 0) { + for (AssetProfileUpdateMsg assetProfileUpdateMsg : uplinkMsg.getAssetProfileUpdateMsgList()) { + result.add(ctx.getAssetProfileProcessor().processAssetProfileMsgFromEdge(edge.getTenantId(), edge, assetProfileUpdateMsg)); + } + } + if (uplinkMsg.getAssetUpdateMsgCount() > 0) { + for (AssetUpdateMsg assetUpdateMsg : uplinkMsg.getAssetUpdateMsgList()) { + result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainUpdateMsgCount() > 0) { + for (RuleChainUpdateMsg ruleChainUpdateMsg : uplinkMsg.getRuleChainUpdateMsgList()) { + result.add(ctx.getRuleChainProcessor().processRuleChainMsgFromEdge(edge.getTenantId(), edge, ruleChainUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) { + for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg : uplinkMsg.getRuleChainMetadataUpdateMsgList()) { + result.add(ctx.getRuleChainProcessor().processRuleChainMetadataMsgFromEdge(edge.getTenantId(), edge, ruleChainMetadataUpdateMsg)); + } + } + if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) { + for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) { + result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg)); + } + } + if (uplinkMsg.getEntityDataCount() > 0) { + for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) { + result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData)); + } + } + if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { + for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { + result.add(ctx.getAlarmProcessor().processAlarmMsgFromEdge(edge.getTenantId(), edge.getId(), alarmUpdateMsg)); + } + } + if (uplinkMsg.getAlarmCommentUpdateMsgCount() > 0) { + for (AlarmCommentUpdateMsg alarmCommentUpdateMsg : uplinkMsg.getAlarmCommentUpdateMsgList()) { + result.add(ctx.getAlarmCommentProcessor().processAlarmCommentMsgFromEdge(edge.getTenantId(), edge.getId(), alarmCommentUpdateMsg)); + } + } + if (uplinkMsg.getRelationUpdateMsgCount() > 0) { + for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { + result.add(ctx.getRelationProcessor().processRelationMsgFromEdge(edge.getTenantId(), edge, relationUpdateMsg)); + } + } + if (uplinkMsg.getDashboardUpdateMsgCount() > 0) { + for (DashboardUpdateMsg dashboardUpdateMsg : uplinkMsg.getDashboardUpdateMsgList()) { + result.add(ctx.getDashboardProcessor().processDashboardMsgFromEdge(edge.getTenantId(), edge, dashboardUpdateMsg)); + } + } + if (uplinkMsg.getResourceUpdateMsgCount() > 0) { + for (ResourceUpdateMsg resourceUpdateMsg : uplinkMsg.getResourceUpdateMsgList()) { + result.add(ctx.getResourceProcessor().processResourceMsgFromEdge(edge.getTenantId(), edge, resourceUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { + for (RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg : uplinkMsg.getRuleChainMetadataRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processRuleChainMetadataRequestMsg(edge.getTenantId(), edge, ruleChainMetadataRequestMsg)); + } + } + if (uplinkMsg.getAttributesRequestMsgCount() > 0) { + for (AttributesRequestMsg attributesRequestMsg : uplinkMsg.getAttributesRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processAttributesRequestMsg(edge.getTenantId(), edge, attributesRequestMsg)); + } + } + if (uplinkMsg.getRelationRequestMsgCount() > 0) { + for (RelationRequestMsg relationRequestMsg : uplinkMsg.getRelationRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg)); + } + } + if (uplinkMsg.getCalculatedFieldRequestMsgCount() > 0) { + for (CalculatedFieldRequestMsg calculatedFieldRequestMsg : uplinkMsg.getCalculatedFieldRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processCalculatedFieldRequestMsg(edge.getTenantId(), edge, calculatedFieldRequestMsg)); + } + } + if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) { + for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg)); + } + } + if (uplinkMsg.getDeviceCredentialsRequestMsgCount() > 0) { + for (DeviceCredentialsRequestMsg deviceCredentialsRequestMsg : uplinkMsg.getDeviceCredentialsRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processDeviceCredentialsRequestMsg(edge.getTenantId(), edge, deviceCredentialsRequestMsg)); + } + } + if (uplinkMsg.getDeviceRpcCallMsgCount() > 0) { + for (DeviceRpcCallMsg deviceRpcCallMsg : uplinkMsg.getDeviceRpcCallMsgList()) { + result.add(ctx.getDeviceProcessor().processDeviceRpcCallFromEdge(edge.getTenantId(), edge, deviceRpcCallMsg)); + } + } + if (uplinkMsg.getWidgetBundleTypesRequestMsgCount() > 0) { + for (WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg : uplinkMsg.getWidgetBundleTypesRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processWidgetBundleTypesRequestMsg(edge.getTenantId(), edge, widgetBundleTypesRequestMsg)); + } + } + if (uplinkMsg.getEntityViewsRequestMsgCount() > 0) { + for (EntityViewsRequestMsg entityViewRequestMsg : uplinkMsg.getEntityViewsRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processEntityViewsRequestMsg(edge.getTenantId(), edge, entityViewRequestMsg)); + } + } + if (uplinkMsg.getCalculatedFieldUpdateMsgCount() > 0) { + for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : uplinkMsg.getCalculatedFieldUpdateMsgList()) { + result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); + } + } + if (uplinkMsg.getAiModelUpdateMsgCount() > 0) { + for (AiModelUpdateMsg aiModelUpdateMsg : uplinkMsg.getAiModelUpdateMsgList()) { + result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg)); + } + } + if (uplinkMsg.getUserUpdateMsgCount() > 0) { + for (UserUpdateMsg userUpdateMsg : uplinkMsg.getUserUpdateMsgList()) { + state.lockSequenceDependency(); + try { + result.add(ctx.getUserProcessor().processUserMsgFromEdge(edge.getTenantId(), edge, userUpdateMsg)); + } finally { + state.unlockSequenceDependency(); + } + } + } + if (uplinkMsg.getUserCredentialsUpdateMsgCount() > 0) { + for (UserCredentialsUpdateMsg userCredentialsUpdateMsg : uplinkMsg.getUserCredentialsUpdateMsgList()) { + state.lockSequenceDependency(); + try { + result.add(ctx.getUserProcessor().processUserCredentialsMsgFromEdge(edge.getTenantId(), edge, userCredentialsUpdateMsg)); + } finally { + state.unlockSequenceDependency(); + } + } + } + } catch (Exception e) { + String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); + log.trace("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), edge.getId(), uplinkMsg, e); + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); + return Futures.immediateFailedFuture(e); + } + return Futures.allAsList(result); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java new file mode 100644 index 0000000000..7da0aecc70 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java @@ -0,0 +1,103 @@ +/** + * 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; + +import io.grpc.Server; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ResourceUtils; +import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; +import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc.EdgeRpcServiceImplBase; +import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.service.edge.rpc.service.EdgeRpcServiceImpl; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.TimeUnit; + +@Component +@Slf4j +@RequiredArgsConstructor +public class GrpcServer { + + @Value("${edges.rpc.port}") + private int rpcPort; + @Value("${edges.rpc.ssl.enabled}") + private boolean sslEnabled; + @Value("${edges.rpc.ssl.cert}") + private String certFileResource; + @Value("${edges.rpc.ssl.private_key}") + private String privateKeyResource; + @Value("${edges.rpc.client_max_keep_alive_time_sec:1}") + private int clientMaxKeepAliveTimeSec; + @Value("${edges.rpc.max_inbound_message_size:4194304}") + private int maxInboundMessageSize; + @Value("${edges.rpc.keep_alive_time_sec:10}") + private int keepAliveTimeSec; + @Value("${edges.rpc.keep_alive_timeout_sec:5}") + private int keepAliveTimeoutSec; + + private final EdgeRpcServiceImplBase edgeGrpcService; + + private Server server; + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void onStartUp() { + log.info("Initializing Edge RPC server!"); + NettyServerBuilder serverBuilder = NettyServerBuilder.forPort(rpcPort) + .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) + .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS) + .keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS) + .permitKeepAliveWithoutCalls(true) + .maxInboundMessageSize(maxInboundMessageSize) + .addService(edgeGrpcService); + + if (sslEnabled) { + addSslConfiguration(serverBuilder); + } + server = serverBuilder.build(); + log.info("Going to start Edge RPC server using port: {}", rpcPort); + try { + server.start(); + } catch (IOException e) { + log.error("Failed to start Edge RPC server!", e); + throw new RuntimeException("Failed to start Edge RPC server!"); + } + log.info("Edge RPC server initialized!"); + } + + private void addSslConfiguration(NettyServerBuilder serverBuilder) { + try { + InputStream certFileIs = ResourceUtils.getInputStream(this, certFileResource); + InputStream privateKeyFileIs = ResourceUtils.getInputStream(this, privateKeyResource); + serverBuilder.useTransportSecurity(certFileIs, privateKeyFileIs); + } catch (Exception e) { + log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); + throw new RuntimeException("Unable to set up SSL context!", e); + } + } + + @PreDestroy + public void destroy() { + if (server != null) { + server.shutdownNow(); + } + } +} 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 deleted file mode 100644 index 63669a8e3d..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ /dev/null @@ -1,184 +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.service.edge.rpc; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import io.grpc.stub.StreamObserver; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.util.ProtoUtils; -import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.ResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; -import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; -import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.kafka.KafkaAdmin; -import org.thingsboard.server.queue.provider.TbCoreQueueFactory; -import org.thingsboard.server.service.edge.EdgeContextComponent; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.function.BiConsumer; - -@Slf4j -public class KafkaEdgeGrpcSession extends EdgeGrpcSession { - - private final TopicService topicService; - private final TbCoreQueueFactory tbCoreQueueFactory; - private final KafkaAdmin kafkaAdmin; - - private volatile boolean isHighPriorityProcessing; - - @Getter - private QueueConsumerManager> consumer; - - private ExecutorService consumerExecutor; - - public KafkaEdgeGrpcSession(EdgeContextComponent ctx, TopicService topicService, TbCoreQueueFactory tbCoreQueueFactory, - KafkaAdmin kafkaAdmin, StreamObserver outputStream, - BiConsumer sessionOpenListener, BiConsumer sessionCloseListener, - ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { - super(ctx, outputStream, sessionOpenListener, sessionCloseListener, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); - this.topicService = topicService; - this.tbCoreQueueFactory = tbCoreQueueFactory; - this.kafkaAdmin = kafkaAdmin; - } - - private void processMsgs(List> msgs, TbQueueConsumer> consumer) { - log.trace("[{}][{}] starting processing edge events", tenantId, edge.getId()); - if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) { - log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " + - "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration", - tenantId, edge.getId(), isConnected(), isSyncInProgress(), isHighPriorityProcessing); - return; - } - List edgeEvents = new ArrayList<>(); - for (TbProtoQueueMsg msg : msgs) { - EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg()); - edgeEvents.add(edgeEvent); - } - List downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents); - try { - boolean 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); - } - } - - @Override - public ListenableFuture migrateEdgeEvents() throws Exception { - return super.processEdgeEvents(); - } - - @Override - public ListenableFuture processEdgeEvents() { - if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) { - log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer", - tenantId, edge != null ? edge.getId() : null, isConnected(), isSyncInProgress(), isHighPriorityProcessing); - return Futures.immediateFuture(Boolean.FALSE); - } - if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) { - try { - if (consumerExecutor != null && !consumerExecutor.isShutdown()) { - try { - consumerExecutor.shutdown(); - awaitConsumerTermination(); - } catch (Exception e) { - log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edge.getId(), e); - } - } - this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); - this.consumer = QueueConsumerManager.>builder() - .name("TB Edge events [" + edge.getId() + "]") - .msgPackProcessor(this::processMsgs) - .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) - .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edge.getId())) - .consumerExecutor(consumerExecutor) - .threadPrefix("edge-events-" + edge.getId()) - .build(); - consumer.subscribe(); - consumer.launch(); - } catch (Exception e) { - destroy(); - log.warn("[{}][{}] Failed to start edge event consumer", sessionId, edge.getId(), e); - } - } - return Futures.immediateFuture(Boolean.FALSE); - } - - @Override - public void processHighPriorityEvents() { - isHighPriorityProcessing = true; - super.processHighPriorityEvents(); - isHighPriorityProcessing = false; - } - - @Override - public boolean destroy() { - try { - if (consumer != null) { - log.info("[{}][{}] Stopping edge event consumer...", tenantId, edge != null ? edge.getId() : null); - consumer.stop(); - } - } catch (Exception e) { - log.warn("[{}][{}] Failed to stop edge event consumer", tenantId, edge.getId(), e); - return false; - } - consumer = null; - try { - if (consumerExecutor != null && !consumerExecutor.isShutdown()) { - consumerExecutor.shutdown(); - awaitConsumerTermination(); - } - } catch (Exception e) { - log.warn("[{}][{}] Failed to shutdown edge event consumer executor", tenantId, edge.getId(), e); - return false; - } - return true; - } - - private void awaitConsumerTermination() { - try { - consumerExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS); - } catch (InterruptedException ie) { - log.warn("[{}][{}] Interrupted while awaiting consumer executor termination", tenantId, edge.getId()); - } - } - - @Override - public void cleanUp() { - String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic(); - kafkaAdmin.deleteTopic(topic); - kafkaAdmin.deleteConsumerGroup(topic); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/PostgresEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/PostgresEdgeGrpcSession.java deleted file mode 100644 index 4cd5bb43e2..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/PostgresEdgeGrpcSession.java +++ /dev/null @@ -1,46 +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.service.edge.rpc; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import io.grpc.stub.StreamObserver; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.gen.edge.v1.ResponseMsg; -import org.thingsboard.server.service.edge.EdgeContextComponent; - -import java.util.UUID; -import java.util.concurrent.ScheduledExecutorService; -import java.util.function.BiConsumer; - -@Slf4j -public class PostgresEdgeGrpcSession extends EdgeGrpcSession { - - PostgresEdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, - BiConsumer sessionOpenListener, - BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, - int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { - super(ctx, outputStream, sessionOpenListener, sessionCloseListener, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); - } - - @Override - public ListenableFuture migrateEdgeEvents() { - return Futures.immediateFuture(Boolean.FALSE); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeEventsDispatcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeEventsDispatcher.java new file mode 100644 index 0000000000..8669737b3b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeEventsDispatcher.java @@ -0,0 +1,23 @@ +/** + * 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.processor; + +import com.google.common.util.concurrent.ListenableFuture; + +public interface EdgeEventsDispatcher { + + ListenableFuture processNewEvents() throws Exception; +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/PostgresGeneralEdgeEventsDispatcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/PostgresGeneralEdgeEventsDispatcher.java new file mode 100644 index 0000000000..c2cd54e5d2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/PostgresGeneralEdgeEventsDispatcher.java @@ -0,0 +1,218 @@ +/** + * 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.processor; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.data.util.Pair; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.session.EdgeSession; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +@Slf4j +@RequiredArgsConstructor +public class PostgresGeneralEdgeEventsDispatcher implements EdgeEventsDispatcher { + + private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; + private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId"; + + private final EdgeSession session; + private final EdgeContextComponent ctx; + + private Long newStartTs; + private Long previousStartTs; + private Long newStartSeqId; + private Long previousStartSeqId; + + @Override + public ListenableFuture processNewEvents() throws Exception { + EdgeSessionState state = session.getState(); + TenantId tenantId = state.getTenantId(); + EdgeId edgeId = state.getEdgeId(); + UUID sessionId = state.getSessionId(); + + SettableFuture result = SettableFuture.create(); + if (state.isConnected() && !state.isSyncInProgress()) { + setLocalPrevStartTsAndSeqId(tenantId, edgeId, sessionId); + GeneralEdgeEventFetcher fetcher = initGeneralEdgeEventFetcher(); + log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", + tenantId, edgeId, previousStartTs, previousStartSeqId); + + Futures.addCallback(session.fetchAndSendEdgeEvents(fetcher), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Pair newStartTsAndSeqId) { + if (newStartTsAndSeqId != null) { + Futures.addCallback(updateQueueStartTsAndSeqId(tenantId, edgeId, newStartTsAndSeqId), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable AttributesSaveResult saveResult) { + log.debug("[{}][{}] queue offset was updated [{}]", tenantId, edgeId, newStartTsAndSeqId); + boolean newEventsAvailable; + if (fetcher.isSeqIdNewCycleStarted()) { + newEventsAvailable = isNewEdgeEventsAvailable(tenantId, edgeId, sessionId); + } else { + newEventsAvailable = isSeqIdStartedNewCycle(tenantId, edgeId, sessionId); + if (!newEventsAvailable) { + newEventsAvailable = isNewEdgeEventsAvailable(tenantId, edgeId, sessionId); + } + } + result.set(newEventsAvailable); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, edgeId, newStartTsAndSeqId, t); + result.setException(t); + } + }, ctx.getGrpcCallbackExecutorService()); + } else { + log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, edgeId); + result.set(Boolean.FALSE); + } + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to process events", tenantId, edgeId, t); + result.setException(t); + } + }, ctx.getGrpcCallbackExecutorService()); + } else { + if (state.isSyncInProgress()) { + log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, edgeId); + result.set(Boolean.TRUE); + } else { + log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, edgeId); + result.set(null); + } + } + return result; + } + + private boolean isSeqIdStartedNewCycle(TenantId tenantId, EdgeId edgeId, UUID sessionId) { + try { + log.trace("[{}][{}][{}] Checking if seq id started new cycle", tenantId, edgeId, sessionId); + TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); + PageData edgeEvents = ctx.getEdgeEventService() + .findEdgeEvents(tenantId, edgeId, 0L, previousStartSeqId == 0 ? null : previousStartSeqId - 1, pageLink); + + boolean result = !edgeEvents.getData().isEmpty(); + log.trace("[{}][{}][{}] Result of check if seq id started new cycle, result = {}", tenantId, edgeId, sessionId, result); + return result; + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", tenantId, edgeId, sessionId, e); + } + return false; + } + + private boolean isNewEdgeEventsAvailable(TenantId tenantId, EdgeId edgeId, UUID sessionId) { + try { + log.trace("[{}][{}][{}] Checking if new edge events available", tenantId, edgeId, sessionId); + TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); + PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(tenantId, edgeId, newStartSeqId, null, pageLink); + boolean available = !edgeEvents.getData().isEmpty() || session.hasHighPriorityEvents(); + log.trace("[{}][{}][{}] Result of check if new edge events available, result = {}", tenantId, edgeId, sessionId, available); + return available; + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", tenantId, edgeId, sessionId, e); + } + return false; + } + + private ListenableFuture updateQueueStartTsAndSeqId(TenantId tenantId, EdgeId edgeId, Pair pair) { + newStartTs = pair.getFirst(); + newStartSeqId = pair.getSecond(); + log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", session.getState().getSessionId(), edgeId, newStartTs, newStartSeqId); + List attributes = List.of( + new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()), + new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, newStartSeqId), System.currentTimeMillis()) + ); + return ctx.getAttributesService().save(tenantId, edgeId, AttributeScope.SERVER_SCOPE, attributes); + } + + private GeneralEdgeEventFetcher initGeneralEdgeEventFetcher() { + return new GeneralEdgeEventFetcher( + previousStartTs, + previousStartSeqId, + false, + Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), + ctx.getEdgeEventService(), + ctx.getEdgeEventStorageSettings().getMisorderingCompensationMillis()); + } + + private void setLocalPrevStartTsAndSeqId(TenantId tenantId, EdgeId edgeId, UUID sessionId) throws InterruptedException, ExecutionException { + Pair startTsAndSeqId = getQueueStartTsAndSeqId(tenantId, edgeId, sessionId).get(); + previousStartTs = startTsAndSeqId.getFirst(); + previousStartSeqId = startTsAndSeqId.getSecond(); + } + + private ListenableFuture> getQueueStartTsAndSeqId(TenantId tenantId, EdgeId edgeId, UUID sessionId) { + ListenableFuture> future = ctx.getAttributesService() + .find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, Arrays.asList(QUEUE_START_TS_ATTR_KEY, QUEUE_START_SEQ_ID_ATTR_KEY)); + + return Futures.transform(future, attributeKvEntries -> { + long startTs = 0L; + long startSeqId = 0L; + for (AttributeKvEntry attributeKvEntry : attributeKvEntries) { + if (QUEUE_START_TS_ATTR_KEY.equals(attributeKvEntry.getKey())) { + startTs = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; + } + if (QUEUE_START_SEQ_ID_ATTR_KEY.equals(attributeKvEntry.getKey())) { + startSeqId = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; + } + } + if (startSeqId == 0L) { + startSeqId = findStartSeqIdFromOldestEventIfAny(tenantId, edgeId, sessionId); + } + return Pair.of(startTs, startSeqId); + }, ctx.getGrpcCallbackExecutorService()); + } + + private long findStartSeqIdFromOldestEventIfAny(TenantId tenantId, EdgeId edgeId, UUID sessionId) { + long startSeqId = 0L; + try { + TimePageLink pageLink = new TimePageLink(1, 0, null, null, null, null); + PageData edgeEvents = ctx.getEdgeEventService() + .findEdgeEvents(tenantId, edgeId, null, null, pageLink); + if (!edgeEvents.getData().isEmpty()) { + startSeqId = edgeEvents.getData().get(0).getSeqId() - 1; + } + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", tenantId, edgeId, sessionId, e); + } + return startSeqId; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeRpcServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeRpcServiceImpl.java new file mode 100644 index 0000000000..14054adbd3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeRpcServiceImpl.java @@ -0,0 +1,386 @@ +/** + * 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.service; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.grpc.stub.StreamObserver; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +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.context.ApplicationContext; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; +import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; +import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; +import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; +import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.session.EdgeAttributeSaveCallback; +import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE; +import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME; +import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME; + +@Service +@Slf4j +@RequiredArgsConstructor +public class EdgeRpcServiceImpl extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { + + @Value("${edges.send_scheduler_pool_size}") + private int sendSchedulerPoolSize; + + @Value("${edges.state.persistToTelemetry:false}") + private boolean persistToTelemetry; + + @Autowired + @Lazy + private EdgeContextComponent ctx; + + private final EdgeSessionsHolder sessions; + private final ApplicationContext applicationContext; + private final TbClusterService clusterService; + private final TbServiceInfoProvider serviceInfoProvider; + private final TelemetrySubscriptionService tsSubService; + private final TbTransactionalCache edgeIdServiceIdCache; + + private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); + private ScheduledExecutorService executorService; + private ScheduledExecutorService sendDownlinkExecutorService; + + @PostConstruct + public void onStartUp() { + this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); + this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); + } + + @PreDestroy + private void preDestroy() { + shutdownExecutorSafely(executorService); + shutdownExecutorSafely(sendDownlinkExecutorService); + sessions.forEach(EdgeGrpcSessionManager::onEdgeDisconnect); + } + + @Override + public StreamObserver handleMsgs(StreamObserver outputStream) { + EdgeGrpcSessionManager sessionManager = applicationContext.getBean(EdgeGrpcSessionManager.class); + return sessionManager.initInputStream(outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService); + } + + @Override + public void onToEdgeSessionMsg(TenantId tenantId, EdgeSessionMsg msg) { + switch (msg.getMsgType()) { + case EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG -> { + EdgeHighPriorityMsg edgeHighPriorityMsg = (EdgeHighPriorityMsg) msg; + log.trace("[{}] edgeEventMsg [{}]", tenantId, msg); + onEdgeHighPriorityEvent(edgeHighPriorityMsg); + } + case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG -> { + EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg; + log.trace("[{}] onToEdgeEventUpdateMsg [{}]", tenantId, msg); + onEdgeEventUpdate(tenantId, edgeEventUpdateMsg.getEdgeId()); + } + case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG -> { + ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg; + log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg); + startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId(), toEdgeSyncRequest.getServiceId()); + } + case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG -> { + FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg; + log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg); + processSyncResponse(fromEdgeSyncResponse); + } + } + } + + @Override + public void updateEdge(TenantId tenantId, Edge edge) { + if (edge == null) { + log.warn("[{}] Edge is null - edge is removed and outdated notification is in process!", tenantId); + return; + } + EdgeGrpcSessionManager session = sessions.getByEdgeId(edge.getId()); + if (session != null && session.getState().isConnected()) { + log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); + session.onConfigurationUpdate(edge); + } else { + log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); + } + } + + @Override + public void deleteEdge(TenantId tenantId, EdgeId edgeId) { + EdgeGrpcSessionManager session = sessions.getByEdgeId(edgeId); + if (session != null && session.getState().isConnected()) { + log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); + session.destroyAndMarkAsZombieIfFailed(); + session.onEdgeRemoval(); + sessions.remove(session); + } + } + + @Override + public void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer) { + ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId, serviceInfoProvider.getServiceId()); + + UUID requestId = request.getId(); + EdgeGrpcSessionManager session = sessions.getByEdgeId(request.getEdgeId()); + if (session != null && session.getState().isSyncInProgress()) { + responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); + } else { + log.trace("[{}][{}] Processing sync edge request [{}], serviceId [{}]", request.getTenantId(), request.getId(), request.getEdgeId(), request.getServiceId()); + localSyncEdgeRequests.put(requestId, responseConsumer); + clusterService.pushEdgeSyncRequestToEdge(request); + scheduleSyncRequestTimeout(request, requestId); + } + } + + private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSessionManager edgeSession) { + EdgeSessionState state = edgeSession.getState(); + Edge edge = state.getEdge(); + TenantId tenantId = state.getTenantId(); + log.info("[{}][{}] edge [{}] connected successfully.", tenantId, state.getSessionId(), edgeId); + if (sessions.hasByEdgeId(edgeId)) { + EdgeGrpcSessionManager existingSession = sessions.getByEdgeId(edgeId); + if (existingSession != null) { + UUID sessionId = existingSession.getState().getSessionId(); + log.info("[{}][{}] Replacing existing session [{}] for edge [{}]", tenantId, state.getSessionId(), sessionId, edgeId); + existingSession.destroyAndMarkAsZombieIfFailed(); + sessions.removeBySessionId(sessionId); + } + } + sessions.put(edgeSession); + save(tenantId, edgeId, ACTIVITY_STATE, true); + long lastConnectTs = System.currentTimeMillis(); + save(tenantId, edgeId, LAST_CONNECT_TIME, lastConnectTs); + edgeIdServiceIdCache.put(edgeId, serviceInfoProvider.getServiceId()); + pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT); + edgeSession.onEdgeConnect(); + } + + private void onEdgeDisconnect(Edge edge, UUID sessionId) { + TenantId tenantId = edge.getTenantId(); + EdgeId edgeId = edge.getId(); + log.info("[{}][{}] edge disconnected!", edgeId, sessionId); + EdgeGrpcSessionManager current = sessions.getByEdgeId(edgeId); + if (current != null && current.getState().getSessionId().equals(sessionId)) { + EdgeGrpcSessionManager toRemove = sessions.removeByEdgeId(edgeId); + toRemove.onEdgeDisconnect(); + toRemove.destroyAndMarkAsZombieIfFailed(); + sessions.removeBySessionId(sessionId); + save(tenantId, edgeId, ACTIVITY_STATE, false); + long lastDisconnectTs = System.currentTimeMillis(); + save(tenantId, edgeId, LAST_DISCONNECT_TIME, lastDisconnectTs); + pushRuleEngineMessage(tenantId, edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); + } else { + log.info("[{}] edge session [{}] is not current anymore. Attempting to destroy it by sessionId.", edgeId, sessionId); + EdgeGrpcSessionManager stale = sessions.removeBySessionId(sessionId); + if (stale != null) { + try { + stale.destroyAndMarkAsZombieIfFailed(); + log.info("[{}][{}] Successfully destroyed stale session for edge [{}]", stale.getState().getTenantId(), sessionId, edgeId); + } catch (Exception e) { + log.warn("[{}][{}] Failed to destroy stale session for edge [{}]", stale.getState().getTenantId(), sessionId, edgeId, e); + } + } else { + log.debug("[{}] No session found by sessionId [{}] to destroy", edgeId, sessionId); + } + } + edgeIdServiceIdCache.evict(edgeId); + } + + private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) { + log.trace("[{}] scheduling sync edge request", requestId); + executorService.schedule(() -> { + log.trace("[{}] checking if sync edge request is not processed...", requestId); + Consumer consumer = localSyncEdgeRequests.remove(requestId); + if (consumer != null) { + log.trace("[{}] timeout for processing sync edge request.", requestId); + consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Edge is not connected")); + } + }, 20, TimeUnit.SECONDS); + } + + private void onEdgeHighPriorityEvent(EdgeHighPriorityMsg msg) { + TenantId tenantId = msg.getTenantId(); + EdgeEvent edgeEvent = msg.getEdgeEvent(); + EdgeId edgeId = edgeEvent.getEdgeId(); + EdgeGrpcSessionManager session = sessions.getByEdgeId(edgeId); + if (session != null && session.getState().isConnected()) { + log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId); + session.addEventToHighPriorityQueue(edgeEvent); + } + } + + private void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { + EdgeGrpcSessionManager session = sessions.getByEdgeId(edgeId); + if (session != null && session.getState().isConnected()) { + log.trace("[{}] onEdgeEventUpdate [{}]", tenantId, edgeId.getId()); + session.onEdgeEventUpdate(); + } + } + + private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId, String requestServiceId) { + EdgeGrpcSessionManager session = sessions.getByEdgeId(edgeId); + if (session != null) { + EdgeSessionState sessionState = session.getState(); + if (sessionState.isSyncInProgress()) { + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"), requestServiceId); + } else { + boolean success = false; + if (sessionState.isConnected()) { + session.startSyncProcess(true); + success = true; + } + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, ""), requestServiceId); + } + } + } + + private void processSyncResponse(FromEdgeSyncResponse response) { + log.trace("[{}] Received response from sync service: [{}]", response.getId(), response); + UUID requestId = response.getId(); + Consumer consumer = localSyncEdgeRequests.remove(requestId); + if (consumer != null) { + consumer.accept(response); + } else { + log.trace("[{}] Unknown or stale sync response received [{}]", requestId, response); + } + } + + private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) { + try { + EdgeId edgeId = edge.getId(); + ObjectNode edgeState = JacksonUtil.newObjectNode(); + boolean isConnected = TbMsgType.CONNECT_EVENT.equals(msgType); + if (isConnected) { + edgeState.put(ACTIVITY_STATE, true); + edgeState.put(LAST_CONNECT_TIME, ts); + } else { + edgeState.put(ACTIVITY_STATE, false); + edgeState.put(LAST_DISCONNECT_TIME, ts); + } + ctx.getRuleProcessor().process(EdgeConnectionTrigger.builder() + .tenantId(tenantId) + .customerId(edge.getCustomerId()) + .edgeId(edgeId) + .edgeName(edge.getName()) + .connected(isConnected).build()); + String data = JacksonUtil.toString(edgeState); + TbMsgMetaData md = new TbMsgMetaData(); + if (!persistToTelemetry) { + md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); + md.putValue("edgeName", edge.getName()); + md.putValue("edgeType", edge.getType()); + } + TbMsg tbMsg = TbMsg.newMsg() + .type(msgType) + .originator(edgeId) + .copyMetaData(md) + .dataType(TbMsgDataType.JSON) + .data(data) + .build(); + clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push {}", tenantId, edge.getId(), msgType, e); + } + } + + private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { + log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value); + if (persistToTelemetry) { + tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .entityId(edgeId) + .entry(new LongDataEntry(key, value)) + .callback(new EdgeAttributeSaveCallback(tenantId, edgeId, key, value)) + .build()); + } else { + tsSubService.saveAttributes(AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(edgeId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(new LongDataEntry(key, value)) + .callback(new EdgeAttributeSaveCallback(tenantId, edgeId, key, value)) + .build()); + } + } + + private void save(TenantId tenantId, EdgeId edgeId, String key, boolean value) { + log.debug("[{}][{}] Updating boolean edge telemetry [{}] [{}]", tenantId, edgeId, key, value); + if (persistToTelemetry) { + tsSubService.saveTimeseries(TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .entityId(edgeId) + .entry(new BooleanDataEntry(key, value)) + .callback(new EdgeAttributeSaveCallback(tenantId, edgeId, key, value)) + .build()); + } else { + tsSubService.saveAttributes(AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(edgeId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(new BooleanDataEntry(key, value)) + .callback(new EdgeAttributeSaveCallback(tenantId, edgeId, key, value)) + .build()); + } + } + + private void shutdownExecutorSafely(ExecutorService sendDownlinkExecutorService) { + if (sendDownlinkExecutorService != null && !sendDownlinkExecutorService.isShutdown()) { + sendDownlinkExecutorService.shutdown(); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/BaseEdgeSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/BaseEdgeSession.java new file mode 100644 index 0000000000..6544128c4f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/BaseEdgeSession.java @@ -0,0 +1,604 @@ +/** + * 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.session; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.data.util.Pair; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; +import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; +import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; +import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.RequestMsgType; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; +import org.thingsboard.server.service.edge.rpc.DownlinkMessageMapper; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.EdgeSyncCursor; +import org.thingsboard.server.service.edge.rpc.EdgeUplinkMessageDispatcher; +import org.thingsboard.server.service.edge.rpc.fetch.EdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; + +@Slf4j +@RequiredArgsConstructor +public class BaseEdgeSession implements EdgeSession { + + private static final int MAX_DOWNLINK_ATTEMPTS = 3; + private static final String RATE_LIMIT_REACHED = "Rate limit reached"; + + private final EdgeGrpcSessionManager parentManagerRef; + private final EdgeContextComponent ctx; + private final StreamObserver outputStream; + private final DownlinkMessageMapper downlinkMessageMapper; + private final EdgeUplinkMessageDispatcher uplinkMessageDispatcher; + private final BiConsumer sessionOpenListener; + private final BiConsumer sessionCloseListener; + private final ScheduledExecutorService sendDownlinkExecutorService; + private final int maxInboundMessageSize; + private final int maxHighPriorityQueueSizePerSession; + + private final EdgeSessionState state = new EdgeSessionState(); + private final Lock downlinkMsgLock = new ReentrantLock(); + private final ConcurrentLinkedQueue highPriorityQueue = new ConcurrentLinkedQueue<>(); + + private int clientMaxInboundMessageSize; + + @Override + public EdgeSessionState getState() { + return state; + } + + @Override + public StreamObserver initInputStream() { + return new StreamObserver<>() { + @Override + public void onNext(RequestMsg requestMsg) { + if (!state.isConnected() && requestMsg.getMsgType().equals(RequestMsgType.CONNECT_RPC_MESSAGE)) { + ConnectResponseMsg responseMsg = processConnect(requestMsg.getConnectRequestMsg()); + outputStream.onNext(ResponseMsg.newBuilder() + .setConnectResponseMsg(responseMsg) + .build()); + if (ConnectResponseCode.ACCEPTED != responseMsg.getResponseCode()) { + outputStream.onError(new RuntimeException(responseMsg.getErrorMsg())); + } else { + if (requestMsg.getConnectRequestMsg().hasMaxInboundMessageSize()) { + log.debug("[{}][{}] Client max inbound message size: {}", + getTenantId(), getSessionId(), requestMsg.getConnectRequestMsg().getMaxInboundMessageSize()); + clientMaxInboundMessageSize = requestMsg.getConnectRequestMsg().getMaxInboundMessageSize(); + } + state.setConnected(true); + } + } + if (state.isConnected()) { + if (requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) { + if (requestMsg.hasSyncRequestMsg()) { + boolean fullSync = false; + if (requestMsg.getSyncRequestMsg().hasFullSync()) { + fullSync = requestMsg.getSyncRequestMsg().getFullSync(); + } + startSyncProcess(fullSync); + } else { + state.finishSync(); + } + } + if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE)) { + if (requestMsg.hasUplinkMsg()) { + onUplinkMsg(requestMsg.getUplinkMsg()); + } + if (requestMsg.hasDownlinkResponseMsg()) { + onDownlinkResponse(requestMsg.getDownlinkResponseMsg()); + } + } + } + } + + @Override + public void onError(Throwable t) { + log.trace("[{}][{}] Stream was terminated due to error:", getTenantId(), getSessionId(), t); + closeSession(); + } + + @Override + public void onCompleted() { + log.info("[{}][{}] Stream was closed and completed successfully!", getTenantId(), getSessionId()); + closeSession(); + } + + private void closeSession() { + state.setConnected(false); + if (state.getEdge() != null) { + try { + sessionCloseListener.accept(state.getEdge(), getSessionId()); + } catch (Exception ignored) { + } + } + try { + outputStream.onCompleted(); + } catch (Exception ignored) { + } + } + }; + } + + @Override + public void startSyncProcess(boolean fullSync) { + if (state.tryStartSync()) { + log.info("[{}][{}][{}] Staring edge sync process", getTenantId(), getEdgeId(), getSessionId()); + interruptGeneralProcessingOnSync(); + doSync(new EdgeSyncCursor(ctx, state.getEdge(), fullSync)); + } else { + log.info("[{}][{}][{}] Sync is already started, skipping starting it now", getTenantId(), getEdgeId(), getSessionId()); + } + } + + @Override + public void sendDownlinkMsg(ResponseMsg responseMsg) { + if (state.isConnected()) { + String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000); + log.trace("[{}][{}] Sending downlink msg [{}]", getTenantId(), getEdgeId(), responseMsgStr); + downlinkMsgLock.lock(); + String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr; + try { + outputStream.onNext(responseMsg); + } catch (Exception e) { + log.trace("[{}][{}] Failed to send downlink message [{}]", getTenantId(), getEdgeId(), downlinkMsgStr, e); + state.setConnected(false); + sessionCloseListener.accept(state.getEdge(), getSessionId()); + } finally { + downlinkMsgLock.unlock(); + } + log.trace("[{}][{}] downlink msg successfully sent [{}]", getTenantId(), getEdgeId(), downlinkMsgStr); + } + } + + @Override + public void addHighPriorityEvent(EdgeEvent edgeEvent) { + while (highPriorityQueue.size() > maxHighPriorityQueueSizePerSession) { + EdgeEvent oldestHighPriority = highPriorityQueue.poll(); + if (oldestHighPriority != null) { + log.warn("[{}][{}][{}] High priority queue is full. Removing oldest high priority event from queue {}", + state.getTenantId(), state.getEdgeId(), getSessionId(), oldestHighPriority); + } + } + highPriorityQueue.add(edgeEvent); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, state.getEdge().getTenantId(), edgeEvent.getEdgeId(), 1)); + } + + @Override + public void processHighPriorityEvents() { + try { + if (state.isConnected() && !state.isSyncInProgress()) { + if (highPriorityQueue.isEmpty()) { + return; + } + List highPriorityEvents = new ArrayList<>(); + EdgeEvent event; + while ((event = highPriorityQueue.poll()) != null) { + highPriorityEvents.add(event); + } + log.trace("[{}][{}] Sending high priority events {}", getTenantId(), getEdgeId(), highPriorityEvents.size()); + List downlinkMsgsPack = downlinkMessageMapper.convertToDownlinkMsgsPack(state, highPriorityEvents); + sendDownlinkMsgsPack(downlinkMsgsPack).get(); + } + } catch (Exception e) { + log.error("[{}] Failed to process high priority events", getEdgeId(), e); + } + } + + @Override + public boolean hasHighPriorityEvents() { + return !highPriorityQueue.isEmpty(); + } + + @Override + public ListenableFuture> fetchAndSendEdgeEvents(EdgeEventFetcher fetcher) { + SettableFuture> result = SettableFuture.create(); + PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()); + fetchAndSendEdgeEvents(fetcher, pageLink, result); + return result; + } + + @Override + public ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { + interruptPreviousSendDownlinkMsgsTask(); + + state.setSendDownlinkMsgsFuture(SettableFuture.create()); + state.getPendingMsgsMap().clear(); + + downlinkMsgsPack.forEach(msg -> state.getPendingMsgsMap().put(msg.getDownlinkMsgId(), msg)); + scheduleDownlinkMsgsPackSend(1); + + return state.getSendDownlinkMsgsFuture(); + } + + @Override + public void close() { + log.debug("[{}][{}] Closing session", getTenantId(), getSessionId()); + state.setConnected(false); + try { + outputStream.onCompleted(); + } catch (Exception e) { + log.debug("[{}][{}] Failed to close output stream: {}", getTenantId(), getSessionId(), e.getMessage()); + } + } + + private void onUplinkMsg(UplinkMsg uplinkMsg) { + if (isRateLimitViolated(uplinkMsg)) { + return; + } + Futures.addCallback(uplinkMessageDispatcher.processUplinkMsg(state, uplinkMsg), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable List result) { + sendResponseMessage(uplinkMsg.getUplinkMsgId(), true, null); + } + + @Override + public void onFailure(Throwable t) { + String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t); + sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg); + } + }, ctx.getGrpcCallbackExecutorService()); + } + + private boolean isRateLimitViolated(UplinkMsg uplinkMsg) { + if (!ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES, getTenantId()) || + !ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES_PER_EDGE, getTenantId(), getEdgeId())) { + String errorMsg = String.format("Failed to process uplink message. %s", RATE_LIMIT_REACHED); + sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg); + return true; + } + return false; + } + + private void sendResponseMessage(int uplinkMsgId, boolean success, String errorMsg) { + UplinkResponseMsg.Builder responseBuilder = UplinkResponseMsg.newBuilder() + .setUplinkMsgId(uplinkMsgId) + .setSuccess(success); + if (errorMsg != null) { + responseBuilder.setErrorMsg(errorMsg); + } + sendDownlinkMsg(ResponseMsg.newBuilder() + .setUplinkResponseMsg(responseBuilder.build()) + .build()); + } + + private void onDownlinkResponse(DownlinkResponseMsg msg) { + try { + if (msg.getSuccess()) { + state.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, getTenantId(), getEdgeId(), 1)); + log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", getTenantId(), getEdgeId(), getSessionId(), msg.getDownlinkMsgId(), msg); + } else { + log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", getTenantId(), getEdgeId(), getSessionId(), msg.getDownlinkMsgId(), msg.getErrorMsg()); + DownlinkMsg downlinkMsg = state.getPendingMsgsMap().get(msg.getDownlinkMsgId()); + // if NOT timeseries or attributes failures - ack failed downlink + if (downlinkMsg.getEntityDataCount() == 0) { + state.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); + } + } + if (state.getPendingMsgsMap().isEmpty()) { + log.debug("[{}][{}][{}] Pending msgs map is empty. Stopping current iteration", getTenantId(), getEdgeId(), getSessionId()); + stopCurrentSendDownlinkMsgsTask(false); + } + } catch (Exception e) { + log.error("[{}][{}] Can't process downlink response message [{}]", getTenantId(), getEdgeId(), msg, e); + } + } + + private void interruptGeneralProcessingOnSync() { + log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", getTenantId(), getEdgeId(), getSessionId()); + stopCurrentSendDownlinkMsgsTask(true); + } + + private void doSync(EdgeSyncCursor cursor) { + if (cursor.hasNext()) { + EdgeEventFetcher next = cursor.getNext(); + log.debug("[{}][{}] starting sync process, cursor current idx = {}, class = {}", + getTenantId(), getEdgeId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); + ListenableFuture> future = fetchAndSendEdgeEvents(next); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Pair result) { + doSync(cursor); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Exception during sync process", getTenantId(), getEdgeId(), t); + } + }, ctx.getGrpcCallbackExecutorService()); + } else { + log.info("[{}][{}] sync process completed", getTenantId(), getEdgeId()); + DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) + .build(); + Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() { + @Override + public void onSuccess(Boolean isInterrupted) { + markSyncCompletedSendEdgeEventUpdate(); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Exception during sending sync complete", getTenantId(), getEdgeId(), t); + markSyncCompletedSendEdgeEventUpdate(); + } + }, ctx.getGrpcCallbackExecutorService()); + } + } + + private void markSyncCompletedSendEdgeEventUpdate() { + state.finishSync(); + ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(getTenantId(), getEdgeId())); + } + + private void fetchAndSendEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) { + Edge edge = state.getEdge(); + TenantId tenantId = getTenantId(); + try { + log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); + processHighPriorityEvents(); + PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); + if (state.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)); + } + log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); + List downlinkMsgsPack = downlinkMessageMapper.convertToDownlinkMsgsPack(state, pageData.getData()); + Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Boolean isInterrupted) { + if (Boolean.TRUE.equals(isInterrupted)) { + log.debug("[{}][{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId(), getSessionId()); + result.set(null); + } else { + if (state.isConnected() && pageData.hasNext()) { + fetchAndSendEdgeEvents(fetcher, pageLink.nextPageLink(), result); + } else { + EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1); + UUID idOffset = latestEdgeEvent.getUuidId(); + if (idOffset != null) { + Long newStartTs = Uuids.unixTimestamp(idOffset); + long newStartSeqId = latestEdgeEvent.getSeqId(); + result.set(Pair.of(newStartTs, newStartSeqId)); + } else { + result.set(null); + } + } + } + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to send downlink msgs pack", edge.getId(), t); + result.setException(t); + } + }, ctx.getGrpcCallbackExecutorService()); + } else { + log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); + result.set(null); + } + } catch (Exception e) { + log.error("[{}] Failed to fetch edge events", edge.getId(), e); + result.setException(e); + } + } + + private void scheduleDownlinkMsgsPackSend(int attempt) { + Runnable sendDownlinkMsgsTask = () -> { + try { + if (!state.isConnected()) { + stopCurrentSendDownlinkMsgsTask(true); + return; + } + if (!state.getPendingMsgsMap().values().isEmpty()) { + Edge edge = state.getEdge(); + List copy = new ArrayList<>(state.getPendingMsgsMap().values()); + if (attempt > 1) { + String error = "Failed to deliver the batch"; + String failureMsg = String.format("{%s} (size: {%s})", error, copy.size()); + if (attempt == 2) { + // Send a failure notification only on the second attempt. + // This ensures that failure alerts are sent just once to avoid redundant notifications. + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(getTenantId()) + .edgeId(getEdgeId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); + } + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED, edge.getTenantId(), getEdgeId(), 1)); + log.warn("[{}][{}] {} on attempt {}", getTenantId(), getEdgeId(), failureMsg, attempt); + log.debug("[{}][{}] entities in failed batch: {}", getTenantId(), getEdgeId(), copy); + } + log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", getTenantId(), getEdgeId(), copy.size()); + for (DownlinkMsg downlinkMsg : copy) { + if (clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > clientMaxInboundMessageSize) { + String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + + "env variable on the edge and restart it.", clientMaxInboundMessageSize); + String message = String.format("Downlink msg size %s exceeds client max inbound message size %s. " + + "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it.", downlinkMsg.getSerializedSize(), clientMaxInboundMessageSize); + log.error("[{}][{}][{}] {} Message {}", getTenantId(), getEdgeId(), getSessionId(), message, downlinkMsg); + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(getTenantId()) + .edgeId(getEdgeId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), getEdgeId(), 1)); + state.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); + } else { + sendDownlinkMsg(ResponseMsg.newBuilder() + .setDownlinkMsg(downlinkMsg) + .build()); + } + } + if (attempt < MAX_DOWNLINK_ATTEMPTS) { + scheduleDownlinkMsgsPackSend(attempt + 1); + } else { + String failureMsg = String.format("Failed to deliver messages: %s", copy); + log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", + getTenantId(), getEdgeId(), MAX_DOWNLINK_ATTEMPTS, copy); + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(getTenantId()).edgeId(getEdgeId()) + .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(), getEdgeId(), copy.size())); + stopCurrentSendDownlinkMsgsTask(false); + } + } else { + stopCurrentSendDownlinkMsgsTask(false); + } + } catch (Exception e) { + log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", getTenantId(), getEdgeId(), e.getMessage(), e); + stopCurrentSendDownlinkMsgsTask(true); + } + }; + + if (attempt == 1) { + sendDownlinkExecutorService.submit(sendDownlinkMsgsTask); + } else { + state.setScheduledSendDownlinkTask( + sendDownlinkExecutorService.schedule( + sendDownlinkMsgsTask, + ctx.getEdgeEventStorageSettings().getSleepIntervalBetweenBatches(), + TimeUnit.MILLISECONDS) + ); + } + } + + private void interruptPreviousSendDownlinkMsgsTask() { + if (state.getSendDownlinkMsgsFuture() != null && !state.getSendDownlinkMsgsFuture().isDone() + || state.getScheduledSendDownlinkTask() != null && !state.getScheduledSendDownlinkTask().isCancelled()) { + log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", + getTenantId(), getEdgeId(), getSessionId()); + stopCurrentSendDownlinkMsgsTask(true); + } else { + log.trace("[{}][{}][{}] Previous send downlink future is not active", + getTenantId(), getEdgeId(), getSessionId()); + } + } + + private void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) { + if (state.getSendDownlinkMsgsFuture() != null && !state.getSendDownlinkMsgsFuture().isDone()) { + state.getSendDownlinkMsgsFuture().set(isInterrupted); + } + if (state.getScheduledSendDownlinkTask() != null) { + state.getScheduledSendDownlinkTask().cancel(true); + } + } + + private ConnectResponseMsg processConnect(ConnectRequestMsg request) { + log.trace("[{}] processConnect [{}]", getSessionId(), request); + Optional optional = ctx.getEdgeService().findEdgeByRoutingKey(TenantId.SYS_TENANT_ID, request.getEdgeRoutingKey()); + if (optional.isPresent()) { + Edge edge = optional.get(); + TenantId tenantId = edge.getTenantId(); + state.setEdge(edge); + try { + if (edge.getSecret().equals(request.getEdgeSecret())) { + sessionOpenListener.accept(edge.getId(), parentManagerRef); + state.setEdgeVersion(request.getEdgeVersion()); + processSaveEdgeVersionAsAttribute(request.getEdgeVersion().name()); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.ACCEPTED) + .setErrorMsg("") + .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)) + .setMaxInboundMessageSize(maxInboundMessageSize) + .build(); + } + String error = "Failed to validate the edge!"; + String failureMsg = String.format("%s Provided request secret: %s", error, request.getEdgeSecret()); + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) + .setErrorMsg(failureMsg) + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } catch (Exception e) { + String failureMsg = "Failed to process edge connection!"; + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); + log.error(failureMsg, e); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE) + .setErrorMsg(failureMsg) + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } + } + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) + .setErrorMsg("Failed to find the edge! Routing key: " + request.getEdgeRoutingKey()) + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } + + private void processSaveEdgeVersionAsAttribute(String edgeVersion) { + AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry(DataConstants.EDGE_VERSION_ATTR_KEY, edgeVersion), System.currentTimeMillis()); + ctx.getAttributesService().save(getTenantId(), getEdgeId(), AttributeScope.SERVER_SCOPE, attributeKvEntry); + } + + private TenantId getTenantId() { + return state.getTenantId(); + } + + private EdgeId getEdgeId() { + return state.getEdgeId(); + } + + private UUID getSessionId() { + return state.getSessionId(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java new file mode 100644 index 0000000000..b9367551fe --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java @@ -0,0 +1,122 @@ +/** + * 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.session; + +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; +import org.thingsboard.server.service.edge.rpc.session.manager.KafkaBasedEdgeGrpcSessionManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +@Service +@Slf4j +public class DefaultZombieSessionCleanupService implements ZombieSessionCleanupService { + + @Autowired + private EdgeSessionsHolder edgeSessionsHolder; + + private final Queue zombieSessions; + private final ScheduledExecutorService zombieSessionsExecutorService; + + public DefaultZombieSessionCleanupService() { + this.zombieSessions = new ConcurrentLinkedQueue<>(); + this.zombieSessionsExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("zombie-sessions"); + this.zombieSessionsExecutorService.scheduleAtFixedRate(this::cleanupZombieSessions, 30, 60, TimeUnit.SECONDS); + } + + @PreDestroy + public void destroy() { + if (zombieSessionsExecutorService != null && !zombieSessionsExecutorService.isShutdown()) { + zombieSessionsExecutorService.shutdown(); + } + } + + @Override + public void add(EdgeGrpcSessionManager session) { + zombieSessions.add(session); + } + + private void cleanupZombieSessions() { + try { + tryToDestroyZombieSessions(getZombieSessions(edgeSessionsHolder.getSessions().values()), + s -> edgeSessionsHolder.removeByEdgeId(s.getState().getEdge().getId())); + + tryToDestroyZombieSessions(getZombieSessions(edgeSessionsHolder.getSessionsById().values()), + s -> edgeSessionsHolder.removeBySessionId(s.getState().getSessionId())); + + zombieSessions.removeIf(zombie -> { + EdgeSessionState state = zombie.getState(); + if (zombie.destroy()) { + log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].", + state.getTenantId(), state.getEdgeId(), state.getSessionId(), state.getEdge().getName()); + return true; + } else { + log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].", + state.getTenantId(), state.getEdgeId(), state.getSessionId(), state.getEdge().getName()); + return false; + } + }); + } catch (Exception e) { + log.warn("Failed to cleanup kafka sessions", e); + } + } + + private List getZombieSessions(Collection sessions) { + List result = new ArrayList<>(); + for (EdgeGrpcSessionManager session : sessions) { + if (isKafkaSessionAndZombie(session)) { + result.add(session); + } + } + return result; + } + + private boolean isKafkaSessionAndZombie(EdgeGrpcSessionManager session) { + if (session instanceof KafkaBasedEdgeGrpcSessionManager kafkaSession) { + EdgeSessionState sessionState = kafkaSession.getState(); + log.debug("[{}] kafkaSession.isConnected() = {}, kafkaSession.getConsumer().getConsumer().isStopped() = {}", + sessionState.getEdgeId(), + sessionState.isConnected(), + kafkaSession.getConsumer() != null ? kafkaSession.getConsumer().getConsumer() != null ? kafkaSession.getConsumer().getConsumer().isStopped() : null : null); + return !sessionState.isConnected() && + kafkaSession.getConsumer() != null && + kafkaSession.getConsumer().getConsumer() != null && + !kafkaSession.getConsumer().getConsumer().isStopped(); + } + return false; + } + + private void tryToDestroyZombieSessions(List sessionsToRemove, Function removeFunc) { + for (EdgeGrpcSessionManager toRemove : sessionsToRemove) { + log.info("[{}] Destroying session for edge because edge is not connected", toRemove.getState().getEdge().getId()); + if (toRemove.destroy()) { + removeFunc.apply(toRemove); + } + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeAttributeSaveCallback.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeAttributeSaveCallback.java new file mode 100644 index 0000000000..c76891db98 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeAttributeSaveCallback.java @@ -0,0 +1,49 @@ +/** + * 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.session; + +import com.google.common.util.concurrent.FutureCallback; +import jakarta.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; + +@Slf4j +public class EdgeAttributeSaveCallback implements FutureCallback { + + private final TenantId tenantId; + private final EdgeId edgeId; + private final String key; + private final Object value; + + public EdgeAttributeSaveCallback(TenantId tenantId, EdgeId edgeId, String key, Object value) { + this.tenantId = tenantId; + this.edgeId = edgeId; + this.key = key; + this.value = value; + } + + @Override + public void onSuccess(@Nullable Void result) { + log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t); + } + +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSessionDelegate.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSessionDelegate.java new file mode 100644 index 0000000000..4386b0ccd7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSessionDelegate.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.service.edge.rpc.session; + +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; + +public abstract class EdgeGrpcSessionDelegate implements EdgeGrpcSessionManager { + + protected abstract EdgeSession getSession(); + + @Override + public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { + getSession().addHighPriorityEvent(edgeEvent); + } + + @Override + public void startSyncProcess(boolean fullSync) { + getSession().startSyncProcess(fullSync); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSession.java new file mode 100644 index 0000000000..6bda6baae7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSession.java @@ -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. + */ +package org.thingsboard.server.service.edge.rpc.session; + +import com.google.common.util.concurrent.ListenableFuture; +import io.grpc.stub.StreamObserver; +import org.springframework.data.util.Pair; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.fetch.EdgeEventFetcher; + +import java.io.Closeable; +import java.util.List; + +public interface EdgeSession extends Closeable { + + StreamObserver initInputStream(); + EdgeSessionState getState(); + void startSyncProcess(boolean fullSync); + void sendDownlinkMsg(ResponseMsg responseMsg); + void addHighPriorityEvent(EdgeEvent edgeEvent); + void processHighPriorityEvents(); + boolean hasHighPriorityEvents(); + ListenableFuture> fetchAndSendEdgeEvents(EdgeEventFetcher fetcher); + ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java new file mode 100644 index 0000000000..5989fd01a1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java @@ -0,0 +1,74 @@ +/** + * 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.session; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; + +@Data +@Slf4j +@Component +public class EdgeSessionsHolder { + + private final ConcurrentMap sessions = new ConcurrentHashMap<>(); + private final ConcurrentMap sessionsById = new ConcurrentHashMap<>(); + + public void forEach(Consumer consumer) { + sessions.values().forEach(consumer); + sessionsById.values().forEach(consumer); + } + + public void put(EdgeGrpcSessionManager session) { + UUID sessionId = session.getState().getSessionId(); + EdgeId edgeId = session.getState().getEdgeId(); + sessionsById.put(sessionId, session); + sessions.put(edgeId, session); + } + + public EdgeGrpcSessionManager getByEdgeId(EdgeId id) { + return sessions.get(id); + } + + public boolean hasByEdgeId(EdgeId id) { + return sessions.containsKey(id); + } + + public EdgeGrpcSessionManager removeByEdgeId(EdgeId id) { + return sessions.remove(id); + } + + public EdgeGrpcSessionManager removeBySessionId(UUID sessionId) { + return sessionsById.remove(sessionId); + } + + public void remove(EdgeGrpcSessionManager session) { + if (session == null) { + log.warn("Can't remove session from holder because it's null"); + } + EdgeSessionState sessionState = session.getState(); + removeByEdgeId(sessionState.getEdge().getId()); // todo: react to warnings + removeBySessionId(sessionState.getSessionId()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/ZombieSessionCleanupService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/ZombieSessionCleanupService.java new file mode 100644 index 0000000000..de6f1fb2d8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/ZombieSessionCleanupService.java @@ -0,0 +1,23 @@ +/** + * 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.session; + +import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; + +public interface ZombieSessionCleanupService { + + void add(EdgeGrpcSessionManager session); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java new file mode 100644 index 0000000000..bda54d5449 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java @@ -0,0 +1,128 @@ +/** + * 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.session.manager; + +import io.grpc.stub.StreamObserver; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.gen.edge.v1.EdgeUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; +import org.thingsboard.server.service.edge.rpc.DownlinkMessageMapper; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.EdgeUplinkMessageDispatcher; +import org.thingsboard.server.service.edge.rpc.session.BaseEdgeSession; +import org.thingsboard.server.service.edge.rpc.session.EdgeGrpcSessionDelegate; +import org.thingsboard.server.service.edge.rpc.session.EdgeSession; +import org.thingsboard.server.service.edge.rpc.session.ZombieSessionCleanupService; + +import java.util.UUID; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; + +@Slf4j +public abstract class AbstractEdgeGrpcSessionManager extends EdgeGrpcSessionDelegate implements EdgeGrpcSessionManager { + + @Value("${edges.max_high_priority_queue_size_per_session:10000}") + protected int maxHighPriorityQueueSizePerSession; + + @Value("${edges.rpc.max_inbound_message_size:4194304}") + protected int maxInboundMessageSize; + + @Autowired + protected ZombieSessionCleanupService zombieSessionCleanupService; + + @Autowired + @Lazy + protected EdgeContextComponent ctx; + + @Autowired + protected DownlinkMessageMapper downlinkMessageMapper; + + @Autowired + protected EdgeUplinkMessageDispatcher uplinkMessageDispatcher; + + @Getter + protected EdgeSession session; + + private final Lock initLock = new ReentrantLock(); + + @Override + public StreamObserver initInputStream(StreamObserver outputStream, + BiConsumer sessionOpenListener, + BiConsumer sessionCloseListener, + ScheduledExecutorService sendDownlinkExecutorService) { + if (session != null) { + throw new IllegalStateException("Session already initialized"); + } + initLock.lock(); + try { + this.session = new BaseEdgeSession( + this, ctx, outputStream, downlinkMessageMapper, uplinkMessageDispatcher, + sessionOpenListener, sessionCloseListener, + sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); + return session.initInputStream(); + } finally { + initLock.unlock(); + } + } + + @Override + public EdgeSessionState getState() { + if (session == null) { + return null; + } + return session.getState(); + } + + @Override + public void onConfigurationUpdate(Edge edge) { + EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() + .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)).build(); + + ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder() + .setEdgeUpdateMsg(edgeConfig) + .build(); + + session.sendDownlinkMsg(edgeConfigMsg); + } + + @Override + public void destroyAndMarkAsZombieIfFailed() { + EdgeSessionState state = getState(); + final var finalSession = session; + + try (finalSession) { + if (!destroy()) { + log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", + state.getTenantId(), state.getEdgeId(), state.getEdge().getName(), state.getSessionId()); + zombieSessionCleanupService.add(this); + } + } catch (Exception e) { + log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]", + state.getTenantId(), state.getEdgeId(), state.getEdge().getName(), state.getSessionId(), e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/EdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/EdgeGrpcSessionManager.java new file mode 100644 index 0000000000..30f9b7111f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/EdgeGrpcSessionManager.java @@ -0,0 +1,47 @@ +/** + * 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.session.manager; + +import io.grpc.stub.StreamObserver; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.gen.edge.v1.RequestMsg; +import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; + +import java.util.UUID; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.BiConsumer; + +public interface EdgeGrpcSessionManager { + + StreamObserver initInputStream(StreamObserver outputStream, + BiConsumer sessionOpenListener, + BiConsumer sessionCloseListener, + ScheduledExecutorService sendDownlinkExecutorService); + EdgeSessionState getState(); + void addEventToHighPriorityQueue(EdgeEvent edgeEvent); + void startSyncProcess(boolean fullSync); + void onEdgeConnect(); + void onEdgeEventUpdate(); + void onConfigurationUpdate(Edge edge); + void onEdgeDisconnect(); + void onEdgeRemoval(); + void destroyAndMarkAsZombieIfFailed(); + boolean destroy(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java new file mode 100644 index 0000000000..44b025b5d7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java @@ -0,0 +1,282 @@ +/** + * 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.session.manager; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.discovery.TopicService; +import org.thingsboard.server.queue.kafka.KafkaAdmin; +import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.queue.util.TbKafkaComponent; +import org.thingsboard.server.service.edge.rpc.DownlinkMessageMapper; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.processor.PostgresGeneralEdgeEventsDispatcher; +import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Service +@Slf4j +@TbKafkaComponent +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@RequiredArgsConstructor +public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionManager { + + private static final int NO_INITIAL_DELAY_VALUE = 0; + + private final TbCoreQueueFactory tbCoreQueueFactory; + private final DownlinkMessageMapper downlinkMessageMapper; + private final TopicService topicService; + private final KafkaAdmin kafkaAdmin; + private final EdgeSessionsHolder sessions; + + private final AtomicReference> initMigrationAndProcessingFutureRef = new AtomicReference<>(); + private final Lock initLock = new ReentrantLock(); + private volatile boolean migrationProcessed; + private volatile boolean isHighPriorityProcessing; + @Getter + private QueueConsumerManager> consumer; + private PostgresGeneralEdgeEventsDispatcher psqlGeneralEdgeEventsDispatcher; + private ExecutorService consumerExecutor; + + @Override + public void onEdgeConnect() { + scheduleMigrationAndProcessing(); + } + + @Override + public void onEdgeEventUpdate() {} + + @Override + public void onEdgeDisconnect() {} + + @Override + public void onEdgeRemoval() { + EdgeSessionState state = getState(); + String topic = topicService + .buildEdgeEventNotificationsTopicPartitionInfo(state.getTenantId(), state.getEdgeId()) + .getTopic(); + kafkaAdmin.deleteTopic(topic); + kafkaAdmin.deleteConsumerGroup(topic); + } + + @Override + public boolean destroy() { + EdgeSessionState state = getState(); + try { + if (consumer != null) { + log.info("[{}][{}] Stopping edge event consumer...", state.getTenantId(), state.getEdgeId()); + consumer.stop(); + } + } catch (Exception e) { + log.warn("[{}][{}] Failed to stop edge event consumer", state.getTenantId(), state.getEdgeId(), e); + return false; + } + consumer = null; + try { + if (consumerExecutor != null && !consumerExecutor.isShutdown()) { + consumerExecutor.shutdown(); + awaitConsumerTermination(); + } + } catch (Exception e) { + log.warn("[{}][{}] Failed to shutdown edge event consumer executor", state.getTenantId(), state.getEdgeId(), e); + return false; + } + return true; + } + + private void scheduleMigrationAndProcessing() { + cancelScheduleEdgeEventsCheck(); + EdgeSessionState state = getSession().getState(); + TenantId tenantId = state.getTenantId(); + EdgeId edgeId = state.getEdgeId(); + + if (!sessions.hasByEdgeId(edgeId)) { + log.debug("[{}] Session was removed and edge event consumer must not be started [{}]", + tenantId, edgeId.getId()); + return; + } + ScheduledFuture initMigrationAndProcessingTask = ctx.getEdgeEventProcessingExecutorService().schedule(() -> { + log.info("[{}] Edge event processing task started for edge [{}]", tenantId, edgeId); + initLock.lock(); + try { + processRemainingPostgresEventsIfRequired(edgeId); + if (migrationProcessed) { + initAndLaunchConsumerWithCallback(tenantId, edgeId, state); + } + } catch (Exception e) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); + } + finally { + initLock.unlock(); + } + }, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); + + initMigrationAndProcessingFutureRef.set(initMigrationAndProcessingTask); + } + + private void initAndLaunchConsumerWithCallback(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { + Futures.transform(initAndLaunchConsumer(tenantId, edgeId, state), success -> { + if (Boolean.FALSE.equals(success)) { + scheduleMigrationAndProcessing(); + } else { + launchProcessingOfHighPriorityEvents(tenantId, edgeId); + } + return null; + }, ctx.getGrpcCallbackExecutorService()); + } + + private void launchProcessingOfHighPriorityEvents(TenantId tenantId, EdgeId edgeId) { + ctx.getEdgeEventProcessingExecutorService().scheduleAtFixedRate(() -> { + try { + isHighPriorityProcessing = true; + session.processHighPriorityEvents(); + isHighPriorityProcessing = false; + } catch (Exception e) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); + } + }, NO_INITIAL_DELAY_VALUE, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); + } + + private ListenableFuture initAndLaunchConsumer(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { + if (!state.isConnected() || state.isSyncInProgress() || isHighPriorityProcessing) { + log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer", + tenantId, edgeId, state.isConnected(), state.isSyncInProgress(), isHighPriorityProcessing); + return Futures.immediateFuture(Boolean.FALSE); + } + if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) { + try { + if (consumerExecutor != null && !consumerExecutor.isShutdown()) { + try { + consumerExecutor.shutdown(); + awaitConsumerTermination(); + } catch (Exception e) { + log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edgeId, e); + } + } + this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); + this.consumer = QueueConsumerManager.>builder() + .name("TB Edge events [" + edgeId + "]") + .msgPackProcessor(this::processMsgs) + .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) + .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edgeId)) + .consumerExecutor(consumerExecutor) + .threadPrefix("edge-events-" + edgeId) + .build(); + consumer.subscribe(); + consumer.launch(); + } catch (Exception e) { + destroy(); + log.warn("[{}][{}] Failed to start edge event consumer", state.getSessionId(), edgeId, e); + } + } + return Futures.immediateFuture(Boolean.TRUE); + } + + private void processMsgs(List> msgs, TbQueueConsumer> consumer) { + EdgeSessionState state = getSession().getState(); + TenantId tenantId = state.getTenantId(); + EdgeId edgeId = state.getEdgeId(); + + log.trace("[{}][{}] starting processing edge events", tenantId, edgeId); + if (!state.isConnected() || state.isSyncInProgress() || isHighPriorityProcessing) { + log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " + + "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration", + tenantId, edgeId, state.isConnected(), state.isSyncInProgress(), isHighPriorityProcessing); + return; + } + List edgeEvents = new ArrayList<>(); + for (TbProtoQueueMsg msg : msgs) { + EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg()); + edgeEvents.add(edgeEvent); + } + List downlinkMsgsPack = downlinkMessageMapper.convertToDownlinkMsgsPack(state, edgeEvents); + try { + boolean isInterrupted = session.sendDownlinkMsgsPack(downlinkMsgsPack).get(); + if (isInterrupted) { + log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edgeId); + } else { + consumer.commit(); + } + } catch (Exception e) { + log.error("[{}][{}] Failed to process downlink messages", tenantId, edgeId, e); + } + } + + private void cancelScheduleEdgeEventsCheck() { + EdgeId edgeId = getState().getEdgeId(); + log.trace("[{}] cancelling edge migration & processing init for edge", edgeId); + + ScheduledFuture sf = initMigrationAndProcessingFutureRef.getAndSet(null); + if (sf != null && !sf.isCancelled() && !sf.isDone()) { + sf.cancel(true); + } + } + + private void processRemainingPostgresEventsIfRequired(EdgeId edgeId) throws Exception { + log.debug("Processing remaining postgres edge events for edge [{}]", edgeId); + + if (psqlGeneralEdgeEventsDispatcher == null) { + psqlGeneralEdgeEventsDispatcher = new PostgresGeneralEdgeEventsDispatcher(session, ctx); + } + if (migrationProcessed) { + return; + } + Boolean hasMorePostgresEvents = psqlGeneralEdgeEventsDispatcher.processNewEvents().get(); + if (Boolean.TRUE.equals(hasMorePostgresEvents)) { + log.info("Migration still in progress for edge [{}]", edgeId.getId()); + scheduleMigrationAndProcessing(); + } else if (Boolean.FALSE.equals(hasMorePostgresEvents)) { + log.info("Migration completed for edge [{}]", edgeId.getId()); + migrationProcessed = true; + } + } + + private void awaitConsumerTermination() { + EdgeSessionState state = getState(); + try { + if (!consumerExecutor.awaitTermination(10, java.util.concurrent.TimeUnit.SECONDS)) { + consumerExecutor.shutdownNow(); // todo: verify it is acceptable + } + } catch (InterruptedException ie) { + log.warn("[{}][{}] Interrupted while awaiting consumer executor termination", state.getTenantId(), state.getEdgeId()); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java new file mode 100644 index 0000000000..616a13ae97 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java @@ -0,0 +1,180 @@ +/** + * 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.session.manager; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.edge.rpc.processor.PostgresGeneralEdgeEventsDispatcher; +import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Service +@Slf4j +@ConditionalOnExpression("'${queue.type:null}'!='kafka'") +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@RequiredArgsConstructor +public class PostgresBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionManager { + + private final EdgeSessionsHolder edgeSessions; + + // todo: verify handled properly + private final Lock newEventsLock = new ReentrantLock(); + private final AtomicReference> edgeEventCheckFutureRef = new AtomicReference<>(); + private PostgresGeneralEdgeEventsDispatcher generalEdgeEventsDispatcher; + private volatile boolean hasNewEvents; + + @Override + public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { + super.addEventToHighPriorityQueue(edgeEvent); + markHasNewEvents(); + } + + @Override + public void onEdgeConnect() { + markHasNewEvents(); + scheduleEdgeEventsCheck(); + } + + @Override + public void onEdgeEventUpdate() { + markHasNewEvents(); + } + + @Override + public void onEdgeDisconnect() { + markHasNoEvents(); + cancelScheduleEdgeEventsCheck(); + } + + @Override + public void onEdgeRemoval() { + markHasNoEvents(); + cancelScheduleEdgeEventsCheck(); + } + + @Override + public boolean destroy() { + markHasNoEvents(); + cancelScheduleEdgeEventsCheck(); + return true; + } + + private void scheduleEdgeEventsCheck() { + cancelScheduleEdgeEventsCheck(); + EdgeId edgeId = getState().getEdgeId(); + TenantId tenantId = getState().getTenantId(); + + if (!edgeSessions.hasByEdgeId(edgeId)) { + log.debug("[{}] Session was removed and edge event check schedule must not be started [{}]", + tenantId, edgeId.getId()); + return; + } + ScheduledFuture edgeEventCheckTask = ctx.getEdgeEventProcessingExecutorService().schedule(() -> { + try { + newEventsLock.lock(); + try { + if (!hasNewEvents) { + scheduleEdgeEventsCheck(); + return; + } + log.trace("[{}][{}] set session new events flag to false", tenantId, edgeId.getId()); + hasNewEvents = false; + session.processHighPriorityEvents(); + processEdgeEventsWithCallback(tenantId, edgeId); + } finally { + newEventsLock.unlock(); + } + } catch (Exception e) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); + } + }, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); + + edgeEventCheckFutureRef.set(edgeEventCheckTask); + log.trace("[{}] Check edge event scheduled for edge [{}]", tenantId, edgeId.getId()); + } + + private void processEdgeEventsWithCallback(TenantId tenantId, EdgeId edgeId) throws Exception { + Futures.addCallback(processEdgeEvents(), new FutureCallback<>() { + @Override + public void onSuccess(Boolean newEventsAdded) { + if (Boolean.TRUE.equals(newEventsAdded)) { + log.trace("[{}][{}] new events added. set session new events flag to true", tenantId, edgeId.getId()); + hasNewEvents = true; + } + scheduleEdgeEventsCheck(); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, t); + scheduleEdgeEventsCheck(); + } + }, ctx.getGrpcCallbackExecutorService()); + } + + public ListenableFuture processEdgeEvents() throws Exception { + if (generalEdgeEventsDispatcher == null) { + generalEdgeEventsDispatcher = new PostgresGeneralEdgeEventsDispatcher(session, ctx); + } + return generalEdgeEventsDispatcher.processNewEvents(); + } + + private void cancelScheduleEdgeEventsCheck() { + EdgeId edgeId = getState().getEdgeId(); + log.trace("[{}] cancelling edge event check for edge", edgeId); + + ScheduledFuture sf = edgeEventCheckFutureRef.getAndSet(null); + if (sf != null && !sf.isCancelled() && !sf.isDone()) { + sf.cancel(true); + } + } + + private void markHasNoEvents() { + markHasEvents(false); + } + + private void markHasNewEvents() { + markHasEvents(true); + } + + private void markHasEvents(boolean newEventsPresent) { + newEventsLock.lock(); + try { + if (!hasNewEvents) { + log.trace("[{}] set session new events flag to {} [{}]", getState().getTenantId(), newEventsPresent, getState().getEdgeId()); + hasNewEvents = newEventsPresent; + } + } finally { + newEventsLock.unlock(); + } + } + +} diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 13c93da411..b1c2a35d36 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -18,9 +18,6 @@ - - - From 6fb7da883ea5b50e9876c150f06713c0e0925621 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Tue, 16 Dec 2025 14:41:33 +0200 Subject: [PATCH 006/313] Undo renaming edge session and edge service --- .../org/thingsboard/server/service/edge/rpc/GrpcServer.java | 2 -- .../service/{EdgeRpcServiceImpl.java => EdgeGrpcService.java} | 2 +- .../session/{BaseEdgeSession.java => EdgeGrpcSession.java} | 2 +- .../rpc/session/manager/AbstractEdgeGrpcSessionManager.java | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) rename application/src/main/java/org/thingsboard/server/service/edge/rpc/service/{EdgeRpcServiceImpl.java => EdgeGrpcService.java} (99%) rename application/src/main/java/org/thingsboard/server/service/edge/rpc/session/{BaseEdgeSession.java => EdgeGrpcSession.java} (99%) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java index 7da0aecc70..70fcdf1f5a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java @@ -23,10 +23,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ResourceUtils; -import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc.EdgeRpcServiceImplBase; import org.thingsboard.server.queue.util.AfterStartUp; -import org.thingsboard.server.service.edge.rpc.service.EdgeRpcServiceImpl; import java.io.IOException; import java.io.InputStream; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeRpcServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java similarity index 99% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeRpcServiceImpl.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java index 14054adbd3..fea5ef4c82 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeRpcServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java @@ -77,7 +77,7 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS @Service @Slf4j @RequiredArgsConstructor -public class EdgeRpcServiceImpl extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { +public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { @Value("${edges.send_scheduler_pool_size}") private int sendSchedulerPoolSize; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/BaseEdgeSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java similarity index 99% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/session/BaseEdgeSession.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java index 6544128c4f..8bb2d36393 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/BaseEdgeSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java @@ -78,7 +78,7 @@ import java.util.function.BiConsumer; @Slf4j @RequiredArgsConstructor -public class BaseEdgeSession implements EdgeSession { +public class EdgeGrpcSession implements EdgeSession { private static final int MAX_DOWNLINK_ATTEMPTS = 3; private static final String RATE_LIMIT_REACHED = "Rate limit reached"; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java index bda54d5449..68d5152306 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java @@ -31,7 +31,7 @@ import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.edge.rpc.DownlinkMessageMapper; import org.thingsboard.server.service.edge.rpc.EdgeSessionState; import org.thingsboard.server.service.edge.rpc.EdgeUplinkMessageDispatcher; -import org.thingsboard.server.service.edge.rpc.session.BaseEdgeSession; +import org.thingsboard.server.service.edge.rpc.session.EdgeGrpcSession; import org.thingsboard.server.service.edge.rpc.session.EdgeGrpcSessionDelegate; import org.thingsboard.server.service.edge.rpc.session.EdgeSession; import org.thingsboard.server.service.edge.rpc.session.ZombieSessionCleanupService; @@ -79,7 +79,7 @@ public abstract class AbstractEdgeGrpcSessionManager extends EdgeGrpcSessionDele } initLock.lock(); try { - this.session = new BaseEdgeSession( + this.session = new EdgeGrpcSession( this, ctx, outputStream, downlinkMessageMapper, uplinkMessageDispatcher, sessionOpenListener, sessionCloseListener, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); From 2b16972e8360ac3bc3252a0d821cd0173a88a344 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Tue, 16 Dec 2025 15:03:47 +0200 Subject: [PATCH 007/313] Fix onConfigurationUpdate by updating edge in local state --- .../rpc/session/manager/AbstractEdgeGrpcSessionManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java index 68d5152306..da502f2584 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java @@ -99,6 +99,10 @@ public abstract class AbstractEdgeGrpcSessionManager extends EdgeGrpcSessionDele @Override public void onConfigurationUpdate(Edge edge) { + EdgeSessionState state = getState(); + if (state != null) { + state.setEdge(edge); + } EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() .setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)).build(); From 89b461050f7ea207baf5cfd417e4d353e0a82346 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Tue, 16 Dec 2025 18:09:17 +0200 Subject: [PATCH 008/313] Refactor KafkaBasedEdgeGrpcSessionManager and introduce highPriorityProcessingFutureRef --- .../KafkaBasedEdgeGrpcSessionManager.java | 107 ++++++++++-------- 1 file changed, 61 insertions(+), 46 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java index 44b025b5d7..95b9b821ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.service.edge.rpc.session.manager; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -46,6 +44,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -68,6 +67,7 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan private final EdgeSessionsHolder sessions; private final AtomicReference> initMigrationAndProcessingFutureRef = new AtomicReference<>(); + private final AtomicReference> highPriorityProcessingFutureRef = new AtomicReference<>(); private final Lock initLock = new ReentrantLock(); private volatile boolean migrationProcessed; private volatile boolean isHighPriorityProcessing; @@ -99,6 +99,7 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan @Override public boolean destroy() { + cancelHighPriorityProcessing(); EdgeSessionState state = getState(); try { if (consumer != null) { @@ -123,7 +124,7 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan } private void scheduleMigrationAndProcessing() { - cancelScheduleEdgeEventsCheck(); + cancelMigrationAndProcessingInit(); EdgeSessionState state = getSession().getState(); TenantId tenantId = state.getTenantId(); EdgeId edgeId = state.getEdgeId(); @@ -139,7 +140,7 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan try { processRemainingPostgresEventsIfRequired(edgeId); if (migrationProcessed) { - initAndLaunchConsumerWithCallback(tenantId, edgeId, state); + initAndLaunchConsumerOrRetry(tenantId, edgeId, state); } } catch (Exception e) { log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); @@ -152,19 +153,17 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan initMigrationAndProcessingFutureRef.set(initMigrationAndProcessingTask); } - private void initAndLaunchConsumerWithCallback(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { - Futures.transform(initAndLaunchConsumer(tenantId, edgeId, state), success -> { - if (Boolean.FALSE.equals(success)) { - scheduleMigrationAndProcessing(); - } else { - launchProcessingOfHighPriorityEvents(tenantId, edgeId); - } - return null; - }, ctx.getGrpcCallbackExecutorService()); + private void initAndLaunchConsumerOrRetry(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { + if (initAndLaunchConsumer(tenantId, edgeId, state)) { + launchProcessingOfHighPriorityEvents(tenantId, edgeId); + } else { + scheduleMigrationAndProcessing(); + } } private void launchProcessingOfHighPriorityEvents(TenantId tenantId, EdgeId edgeId) { - ctx.getEdgeEventProcessingExecutorService().scheduleAtFixedRate(() -> { + cancelHighPriorityProcessing(); + ScheduledFuture highPriorityProcessingTask = ctx.getEdgeEventProcessingExecutorService().scheduleAtFixedRate(() -> { try { isHighPriorityProcessing = true; session.processHighPriorityEvents(); @@ -173,41 +172,50 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); } }, NO_INITIAL_DELAY_VALUE, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); + highPriorityProcessingFutureRef.set(highPriorityProcessingTask); } - private ListenableFuture initAndLaunchConsumer(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { + private boolean initAndLaunchConsumer(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { if (!state.isConnected() || state.isSyncInProgress() || isHighPriorityProcessing) { log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer", tenantId, edgeId, state.isConnected(), state.isSyncInProgress(), isHighPriorityProcessing); - return Futures.immediateFuture(Boolean.FALSE); + return false; } - if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) { - try { - if (consumerExecutor != null && !consumerExecutor.isShutdown()) { - try { - consumerExecutor.shutdown(); - awaitConsumerTermination(); - } catch (Exception e) { - log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edgeId, e); - } + if (isConsumerInitializationRequired()) { + initConsumerAndExecutor(tenantId, edgeId, state); + } + return true; + } + + private void initConsumerAndExecutor(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { + try { + if (consumerExecutor != null && !consumerExecutor.isShutdown()) { + try { + consumerExecutor.shutdown(); + awaitConsumerTermination(); + } catch (Exception e) { + log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edgeId, e); } - this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); - this.consumer = QueueConsumerManager.>builder() - .name("TB Edge events [" + edgeId + "]") - .msgPackProcessor(this::processMsgs) - .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) - .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edgeId)) - .consumerExecutor(consumerExecutor) - .threadPrefix("edge-events-" + edgeId) - .build(); - consumer.subscribe(); - consumer.launch(); - } catch (Exception e) { - destroy(); - log.warn("[{}][{}] Failed to start edge event consumer", state.getSessionId(), edgeId, e); } + this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); + this.consumer = QueueConsumerManager.>builder() + .name("TB Edge events [" + edgeId + "]") + .msgPackProcessor(this::processMsgs) + .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) + .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edgeId)) + .consumerExecutor(consumerExecutor) + .threadPrefix("edge-events-" + edgeId) + .build(); + consumer.subscribe(); + consumer.launch(); + } catch (Exception e) { + destroy(); + log.warn("[{}][{}] Failed to start edge event consumer", state.getSessionId(), edgeId, e); } - return Futures.immediateFuture(Boolean.TRUE); + } + + private boolean isConsumerInitializationRequired() { + return consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped()); } private void processMsgs(List> msgs, TbQueueConsumer> consumer) { @@ -240,13 +248,20 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan } } - private void cancelScheduleEdgeEventsCheck() { - EdgeId edgeId = getState().getEdgeId(); - log.trace("[{}] cancelling edge migration & processing init for edge", edgeId); + private void cancelMigrationAndProcessingInit() { + log.trace("[{}] cancelling edge migration & processing init for edge", getState().getEdgeId()); + cancelFuture(initMigrationAndProcessingFutureRef); + } + + private void cancelHighPriorityProcessing() { + log.trace("[{}] cancelling high priority processing task for edge", getState().getEdgeId()); + cancelFuture(highPriorityProcessingFutureRef); + } - ScheduledFuture sf = initMigrationAndProcessingFutureRef.getAndSet(null); - if (sf != null && !sf.isCancelled() && !sf.isDone()) { - sf.cancel(true); + private void cancelFuture(AtomicReference> futureReference) { + Future f = futureReference.getAndSet(null); + if (f != null && !f.isCancelled() && !f.isDone()) { + f.cancel(true); } } From 70ba4b03e4f1d8cb81a25c251027a560f15bea6b Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Tue, 16 Dec 2025 19:01:12 +0200 Subject: [PATCH 009/313] Fix infinite loop on Kafka consumer commit failure --- .../session/manager/KafkaBasedEdgeGrpcSessionManager.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java index 95b9b821ec..cf354b8877 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java @@ -236,16 +236,18 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan edgeEvents.add(edgeEvent); } List downlinkMsgsPack = downlinkMessageMapper.convertToDownlinkMsgsPack(state, edgeEvents); + boolean isInterrupted = true; try { - boolean isInterrupted = session.sendDownlinkMsgsPack(downlinkMsgsPack).get(); + isInterrupted = session.sendDownlinkMsgsPack(downlinkMsgsPack).get(); if (isInterrupted) { log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edgeId); - } else { - consumer.commit(); } } catch (Exception e) { log.error("[{}][{}] Failed to process downlink messages", tenantId, edgeId, e); } + if (!isInterrupted) { + consumer.commit(); + } } private void cancelMigrationAndProcessingInit() { From 88299204be6ac0fdfbe1610fa7b15985f89f2694 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Wed, 17 Dec 2025 11:44:16 +0200 Subject: [PATCH 010/313] Fix statsCounterService in EdgeGrpcSession after merge --- .../server/service/edge/rpc/session/EdgeGrpcSession.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java index 8bb2d36393..64989b8c5a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java @@ -401,7 +401,8 @@ public class EdgeGrpcSession implements EdgeSession { if (state.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 = downlinkMessageMapper.convertToDownlinkMsgsPack(state, pageData.getData()); @@ -496,7 +497,8 @@ public class EdgeGrpcSession implements EdgeSession { ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(getTenantId()).edgeId(getEdgeId()) .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(), getEdgeId(), copy.size())); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), getEdgeId(), copy.size())); stopCurrentSendDownlinkMsgsTask(false); } } else { From e0fbbf6b4e32ded0cf8f57cd1b389d32e75da748 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 6 Jan 2026 15:10:16 +0200 Subject: [PATCH 011/313] REST API call node: support duplicate query param names --- .../common/data/util/CollectionsUtil.java | 10 -- .../rule/engine/rest/QueryParam.java | 23 +++ .../rule/engine/rest/TbHttpClient.java | 27 ++-- .../rest/TbRestApiCallNodeConfiguration.java | 6 +- .../rule/engine/rest/TbHttpClientTest.java | 146 +++++++++--------- .../engine/rest/TbRestApiCallNodeTest.java | 27 +++- 6 files changed, 136 insertions(+), 103 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java 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 bdc4430387..082be9b71f 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 @@ -18,7 +18,6 @@ package org.thingsboard.server.common.data.util; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -73,15 +72,6 @@ public class CollectionsUtil { return map; } - @SafeVarargs - public static LinkedHashMap orderedMapOf(Map.Entry... entries) { - LinkedHashMap map = new LinkedHashMap<>(); - for (Map.Entry entry : entries) { - map.put(entry.getKey(), entry.getValue()); - } - return map; - } - public static boolean emptyOrContains(Collection collection, V element) { return isEmpty(collection) || collection.contains(element); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java new file mode 100644 index 0000000000..2a72b3a88a --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java @@ -0,0 +1,23 @@ +/** + * 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.rest; + +import jakarta.validation.constraints.NotNull; + +record QueryParam( + @NotNull(message = "query parameter names must be non-null") String name, + @NotNull(message = "query parameter values must be non-null") String value +) {} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index a16b3f5fb5..0d5a78810b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.rest; -import com.google.common.collect.Maps; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.ssl.SslContext; @@ -217,14 +216,15 @@ public class TbHttpClient { String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg); - Map processedQueryParams; + List processedQueryParams; if (config.isUseNewEncoding()) { - processedQueryParams = Maps.newHashMapWithExpectedSize(config.getQueryParams().size()); - config.getQueryParams().forEach((name, value) -> { - var processedParamName = TbNodeUtils.processPattern(name, msg); - var processedParamValue = TbNodeUtils.processPattern(value, msg); - processedQueryParams.put(processedParamName, processedParamValue); - }); + processedQueryParams = config.getQueryParams().stream() + .map(param -> { + var processedParamName = TbNodeUtils.processPattern(param.name(), msg); + var processedParamValue = TbNodeUtils.processPattern(param.value(), msg); + return new QueryParam(processedParamName, processedParamValue); + }) + .toList(); } else { processedQueryParams = null; } @@ -278,7 +278,7 @@ public class TbHttpClient { return origin; } - public URI buildEncodedUri(String endpointUrl, Map queryParams) { + public URI buildEncodedUri(String endpointUrl, List queryParams) { if (endpointUrl == null) { throw new RuntimeException("Url string cannot be null!"); } @@ -306,16 +306,15 @@ public class TbHttpClient { return uri; } - private URI buildEncodedUriNew(String endpointUrl, Map queryParams) { + private URI buildEncodedUriNew(String endpointUrl, List queryParams) { try { URIBuilder builder = new URIBuilder(endpointUrl); - queryParams.forEach(builder::addParameter); + queryParams.forEach(param -> builder.addParameter(param.name(), param.value())); return builder.build(); } catch (URISyntaxException e) { throw new IllegalArgumentException(""" - Invalid REST endpoint URL: '%s'. The URL must be valid and properly encoded. - If the path contains special characters (e.g., spaces, braces), they must be percent-encoded (e.g., '/my file/' should be '/my%%20file/', '/path/{id}/' should be '/path/%%7Bid%%7D/'). - Query parameters should be configured separately and will be encoded automatically.""".formatted(endpointUrl), e); + Invalid request URL: '%s'. The URL must be valid and properly encoded. + If URL contains special characters (e.g., spaces), they must be percent-encoded (e.g., '/my file/' should be '/my%%20file/').""".formatted(endpointUrl), e); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index c896dea324..cd12eb5211 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.rest; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.Valid; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -27,6 +28,7 @@ import org.thingsboard.rule.engine.credentials.AnonymousCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials; import java.util.Collections; +import java.util.List; import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) @@ -36,7 +38,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration queryParams; + private List<@NotNull @Valid QueryParam> queryParams; private Map headers; private boolean useSimpleClientHttpFactory; private int readTimeoutMs; @@ -78,7 +80,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration queryParams, String expectedEncodedUrl) { + public void testQueryParamsEncoding(String endpointUrl, List queryParams, String expectedEncodedUrl) { // GIVEN Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); @@ -253,42 +251,42 @@ public class TbHttpClientTest { return Stream.of( Arguments.of( Named.named("ISO 8601 date-time in value", "http://somecompany/api/data/fetch"), - Map.of("ts", "2016-08-01T09:06:06.0+02:00"), + List.of(new QueryParam("ts", "2016-08-01T09:06:06.0+02:00")), "http://somecompany/api/data/fetch?ts=2016-08-01T09%3A06%3A06.0%2B02%3A00" ), Arguments.of( Named.named("email with plus sign in value", "http://localhost:8080/api/user/sendActivationMail"), - Map.of("email", "someperson+test1289@thingsboard.io"), + List.of(new QueryParam("email", "someperson+test1289@thingsboard.io")), "http://localhost:8080/api/user/sendActivationMail?email=someperson%2Btest1289%40thingsboard.io" ), Arguments.of( Named.named("plus mixed with spaces in value", "http://url/api"), - Map.of("q", "a + b"), + List.of(new QueryParam("q", "a + b")), "http://url/api?q=a%20%2B%20b" ), Arguments.of( Named.named("colon in value", "http://url/api"), - Map.of("time", "12:00"), + List.of(new QueryParam("time", "12:00")), "http://url/api?time=12%3A00" ), Arguments.of( Named.named("slash in value", "http://url"), - Map.of("ref", "/home/user"), + List.of(new QueryParam("ref", "/home/user")), "http://url?ref=%2Fhome%2Fuser" ), Arguments.of( Named.named("comma and semicolon in value", "http://url"), - Map.of("l", "a,b;c"), + List.of(new QueryParam("l", "a,b;c")), "http://url?l=a%2Cb%3Bc" ), Arguments.of( Named.named("ampersand and equals in value", "http://url"), - Map.of("q", "key1=value1&key2=value2"), + List.of(new QueryParam("q", "key1=value1&key2=value2")), "http://url?q=key1%3Dvalue1%26key2%3Dvalue2" ), Arguments.of( Named.named("JSON in value", "http://url"), - Map.of("json", """ + List.of(new QueryParam("json", """ { "string": "hello", "integer": 42, @@ -305,245 +303,253 @@ public class TbHttpClientTest { "object": { "nested": "value" } - }"""), + }""")), "http://url?json=%7B%0A%20%20%20%20%22string%22%3A%20%22hello%22%2C%0A%20%20%20%20%22integer%22%3A%2042%2C%0A%20%20%20%20%22float%22%3A%203.14%2C%0A%20%20%20%20%22boolTrue%22%3A%20true%2C%0A%20%20%20%20%22boolFalse%22%3A%20false%2C%0A%20%20%20%20%22null%22%3A%20null%2C%0A%20%20%20%20%22array%22%3A%20%5B%0A%20%20%20%20%20%20%20%201%2C%0A%20%20%20%20%20%20%20%20%22two%22%2C%0A%20%20%20%20%20%20%20%20true%2C%0A%20%20%20%20%20%20%20%20null%0A%20%20%20%20%5D%2C%0A%20%20%20%20%22object%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22nested%22%3A%20%22value%22%0A%20%20%20%20%7D%0A%7D" ), Arguments.of( Named.named("UTF-8 in query", "http://url/cafes"), - orderedMapOf( - entry("nom", "Le Goût Moderne"), - entry("назва", "У Миколи \uD83D\uDE0B") + List.of( + new QueryParam("nom", "Le Goût Moderne"), + new QueryParam("назва", "У Миколи \uD83D\uDE0B") ), "http://url/cafes?nom=Le%20Go%C3%BBt%20Moderne&%D0%BD%D0%B0%D0%B7%D0%B2%D0%B0=%D0%A3%20%D0%9C%D0%B8%D0%BA%D0%BE%D0%BB%D0%B8%20%F0%9F%98%8B" ), Arguments.of( Named.named("empty value", "http://url/empty"), - Map.of("name", ""), + List.of(new QueryParam("name", "")), "http://url/empty?name=" ), Arguments.of( Named.named("blank value (spaces)", "http://url/empty"), - Map.of("name", " "), + List.of(new QueryParam("name", " ")), "http://url/empty?name=%20%20" ), Arguments.of( Named.named("empty key", "http://url/empty"), - Map.of("", "value"), + List.of(new QueryParam("", "value")), "http://url/empty?=value" ), Arguments.of( Named.named("blank key (spaces)", "http://url/empty"), - Map.of(" ", "value"), + List.of(new QueryParam(" ", "value")), "http://url/empty?%20%20=value" ), Arguments.of( Named.named("blank key with value that needs to be encoded", "http://url/empty"), - Map.of(" ", "value1+value2"), + List.of(new QueryParam(" ", "value1+value2")), "http://url/empty?%20%20=value1%2Bvalue2" ), Arguments.of( Named.named("blank key and value", "http://url/empty"), - Map.of(" ", " "), + List.of(new QueryParam(" ", " ")), "http://url/empty?%20%20=%20%20" ), Arguments.of( Named.named("fragment with query params", "http://url#frag"), - Map.of("docs", "مستندات عقدة القاعدة\n"), + List.of(new QueryParam("docs", "مستندات عقدة القاعدة\n")), "http://url?docs=%D9%85%D8%B3%D8%AA%D9%86%D8%AF%D8%A7%D8%AA%20%D8%B9%D9%82%D8%AF%D8%A9%20%D8%A7%D9%84%D9%82%D8%A7%D8%B9%D8%AF%D8%A9%0A#frag" ), Arguments.of( Named.named("fragment only", "http://url#frag"), - Collections.emptyMap(), + Collections.emptyList(), "http://url#frag" ), Arguments.of( Named.named("multiple query params (ordered)", "http://url/api"), - orderedMapOf( - entry("param1", "value1"), - entry("param2", "value2"), - entry("param3", "value3") + List.of( + new QueryParam("param1", "value1"), + new QueryParam("param2", "value2"), + new QueryParam("param3", "value3") ), "http://url/api?param1=value1¶m2=value2¶m3=value3" ), Arguments.of( Named.named("hash (#) in value", "http://url/api"), - Map.of("color", "#ff0000"), + List.of(new QueryParam("color", "#ff0000")), "http://url/api?color=%23ff0000" ), Arguments.of( Named.named("question mark (?) in value", "http://url/api"), - Map.of("query", "what?"), + List.of(new QueryParam("query", "what?")), "http://url/api?query=what%3F" ), Arguments.of( Named.named("percent sign (%) in value", "http://url/api"), - Map.of("discount", "50%"), + List.of(new QueryParam("discount", "50%")), "http://url/api?discount=50%25" ), Arguments.of( Named.named("already encoded string - double encoding", "http://url/api"), - Map.of("encoded", "%20"), + List.of(new QueryParam("encoded", "%20")), "http://url/api?encoded=%2520" ), Arguments.of( Named.named("URL with port number", "http://localhost:8080/api/v1/data"), - Map.of("key", "value"), + List.of(new QueryParam("key", "value")), "http://localhost:8080/api/v1/data?key=value" ), Arguments.of( Named.named("URL with userinfo (auth)", "http://user:password@hostname/path"), - Map.of("secure", "true"), + List.of(new QueryParam("secure", "true")), "http://user:password@hostname/path?secure=true" ), Arguments.of( Named.named("IPv4 address in URL", "http://192.168.1.100:9090/endpoint"), - Map.of("ip", "test"), + List.of(new QueryParam("ip", "test")), "http://192.168.1.100:9090/endpoint?ip=test" ), Arguments.of( Named.named("IPv6 address in URL", "http://[::1]:8080/api"), - Map.of("ipv6", "true"), + List.of(new QueryParam("ipv6", "true")), "http://[::1]:8080/api?ipv6=true" ), Arguments.of( Named.named("HTTPS protocol", "https://secure.example.com/api"), - Map.of("token", "abc123"), + List.of(new QueryParam("token", "abc123")), "https://secure.example.com/api?token=abc123" ), Arguments.of( Named.named("pipe (|) in value", "http://url/api"), - Map.of("filter", "a|b|c"), + List.of(new QueryParam("filter", "a|b|c")), "http://url/api?filter=a%7Cb%7Cc" ), Arguments.of( Named.named("caret (^) and backtick (`) in value", "http://url/api"), - Map.of("special", "a^b`c"), + List.of(new QueryParam("special", "a^b`c")), "http://url/api?special=a%5Eb%60c" ), Arguments.of( Named.named("tab character in value", "http://url/api"), - Map.of("data", "col1\tcol2\tcol3"), + List.of(new QueryParam("data", "col1\tcol2\tcol3")), "http://url/api?data=col1%09col2%09col3" ), Arguments.of( Named.named("CRLF in value", "http://url/api"), - Map.of("text", "line1\r\nline2"), + List.of(new QueryParam("text", "line1\r\nline2")), "http://url/api?text=line1%0D%0Aline2" ), Arguments.of( Named.named("square brackets in value", "http://url/api"), - Map.of("array", "[1,2,3]"), + List.of(new QueryParam("array", "[1,2,3]")), "http://url/api?array=%5B1%2C2%2C3%5D" ), Arguments.of( Named.named("single and double quotes in value", "http://url/api"), - Map.of("quoted", "He said \"Hello\" and 'Hi'"), + List.of(new QueryParam("quoted", "He said \"Hello\" and 'Hi'")), "http://url/api?quoted=He%20said%20%22Hello%22%20and%20%27Hi%27" ), Arguments.of( Named.named("angle brackets in value (XSS test)", "http://url/api"), - Map.of("html", ""), + List.of(new QueryParam("html", "")), "http://url/api?html=%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E" ), Arguments.of( Named.named("backslash in value", "http://url/api"), - Map.of("path", "C:\\Users\\test"), + List.of(new QueryParam("path", "C:\\Users\\test")), "http://url/api?path=C%3A%5CUsers%5Ctest" ), Arguments.of( Named.named("at sign (@) in value", "http://url/api"), - Map.of("contact", "user@domain.com"), + List.of(new QueryParam("contact", "user@domain.com")), "http://url/api?contact=user%40domain.com" ), Arguments.of( Named.named("URL as value", "http://url/api"), - Map.of("redirect", "https://example.com/path?foo=bar"), + List.of(new QueryParam("redirect", "https://example.com/path?foo=bar")), "http://url/api?redirect=https%3A%2F%2Fexample.com%2Fpath%3Ffoo%3Dbar" ), Arguments.of( - Named.named("empty map (not null)", "http://url/api"), - Collections.emptyMap(), + Named.named("empty list (not null)", "http://url/api"), + Collections.emptyList(), "http://url/api" ), Arguments.of( Named.named("underscore and hyphen in key/value", "http://url/api"), - Map.of("my-key_name", "my-value_data"), + List.of(new QueryParam("my-key_name", "my-value_data")), "http://url/api?my-key_name=my-value_data" ), Arguments.of( Named.named("dots in key and value", "http://url/api"), - Map.of("version.major", "1.2.3"), + List.of(new QueryParam("version.major", "1.2.3")), "http://url/api?version.major=1.2.3" ), Arguments.of( Named.named("multiple reserved chars combined", "http://url/api"), - Map.of("complex", "a=1&b=2#section?query"), + List.of(new QueryParam("complex", "a=1&b=2#section?query")), "http://url/api?complex=a%3D1%26b%3D2%23section%3Fquery" ), Arguments.of( Named.named("trailing slash in URL", "http://url/api/"), - Map.of("param", "value"), + List.of(new QueryParam("param", "value")), "http://url/api/?param=value" ), Arguments.of( Named.named("long value (1000 chars)", "http://url/api"), - Map.of("long", "a".repeat(1000)), + List.of(new QueryParam("long", "a".repeat(1000))), "http://url/api?long=" + "a".repeat(1000) ), Arguments.of( Named.named("Chinese characters in value", "http://url/api"), - Map.of("greeting", "你好世界"), + List.of(new QueryParam("greeting", "你好世界")), "http://url/api?greeting=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C" ), Arguments.of( Named.named("Japanese characters in value", "http://url/api"), - Map.of("text", "こんにちは"), + List.of(new QueryParam("text", "こんにちは")), "http://url/api?text=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF" ), Arguments.of( Named.named("existing query params in URL", "http://url/api?existing=param"), - Map.of("new", "value"), + List.of(new QueryParam("new", "value")), "http://url/api?existing=param&new=value" ), Arguments.of( Named.named("existing query and fragment in URL", "http://url/api?existing=param#section"), - Map.of("additional", "data"), + List.of(new QueryParam("additional", "data")), "http://url/api?existing=param&additional=data#section" ), Arguments.of( Named.named("null byte in value", "http://url/api"), - Map.of("data", "before\u0000after"), + List.of(new QueryParam("data", "before\u0000after")), "http://url/api?data=before%00after" ), Arguments.of( Named.named("form feed and vertical tab in value", "http://url/api"), - Map.of("whitespace", "a\fb\u000Bc"), + List.of(new QueryParam("whitespace", "a\fb\u000Bc")), "http://url/api?whitespace=a%0Cb%0Bc" ), Arguments.of( Named.named("tilde (unreserved, not encoded)", "http://url/api"), - Map.of("pattern", "~user"), + List.of(new QueryParam("pattern", "~user")), "http://url/api?pattern=~user" ), Arguments.of( Named.named("asterisk in value", "http://url/api"), - Map.of("wildcard", "*.txt"), + List.of(new QueryParam("wildcard", "*.txt")), "http://url/api?wildcard=%2A.txt" ), Arguments.of( Named.named("curly braces in key", "http://url/api"), - Map.of("{key}", "value"), + List.of(new QueryParam("{key}", "value")), "http://url/api?%7Bkey%7D=value" ), Arguments.of( Named.named("curly braces in value", "http://url/api"), - Map.of("data", "{value}"), + List.of(new QueryParam("data", "{value}")), "http://url/api?data=%7Bvalue%7D" ), Arguments.of( Named.named("curly braces in both key and value", "http://url/api"), - Map.of("{param}", "{data}"), + List.of(new QueryParam("{param}", "{data}")), "http://url/api?%7Bparam%7D=%7Bdata%7D" + ), + Arguments.of( + Named.named("duplicate param names with different values", "http://url/api"), + List.of( + new QueryParam("test", "a+b"), + new QueryParam("test", "b c") + ), + "http://url/api?test=a%2Bb&test=b%20c" ) ); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index 8c3654bdc5..39d9e0d585 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -52,8 +52,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import java.io.IOException; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -120,14 +119,28 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { } } + @Test + public void shouldNotAllowNullQueryParamNames() { + // GIVEN + var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setUseNewEncoding(true); + config.setQueryParams(List.of(new QueryParam(null, "value"))); + + // WHEN-THEN + assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .matches(e -> ((TbNodeException) e).isUnrecoverable()) + .rootCause() + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("query parameter names must be non-null"); + } + @Test public void shouldNotAllowNullQueryParamValues() { // GIVEN var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); config.setUseNewEncoding(true); - var params = new HashMap(); - params.put("key", null); - config.setQueryParams(params); + config.setQueryParams(List.of(new QueryParam("key", null))); // WHEN-THEN assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) @@ -143,7 +156,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { // GIVEN var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); config.setUseNewEncoding(false); - config.setQueryParams(Map.of("key", "value")); + config.setQueryParams(List.of(new QueryParam("key", "value"))); // WHEN-THEN assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) @@ -175,7 +188,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { // THEN assertTrue(defaultConfig.isUseNewEncoding()); - assertEquals(Collections.emptyMap(), defaultConfig.getQueryParams()); + assertEquals(Collections.emptyList(), defaultConfig.getQueryParams()); } @Test From df795e252b7d0bac6948a062240c7205139a9bb1 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 7 Jan 2026 11:59:46 +0200 Subject: [PATCH 012/313] REST API call node: refactor to avoid boolean flag --- .../rule/engine/rest/TbHttpClient.java | 12 +++---- .../rest/TbRestApiCallNodeConfiguration.java | 22 ------------ .../rule/engine/rest/TbHttpClientTest.java | 2 -- .../engine/rest/TbRestApiCallNodeTest.java | 35 ++++++------------- 4 files changed, 16 insertions(+), 55 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 0d5a78810b..e9476157aa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -217,7 +217,7 @@ public class TbHttpClient { String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg); List processedQueryParams; - if (config.isUseNewEncoding()) { + if (config.getQueryParams() != null) { processedQueryParams = config.getQueryParams().stream() .map(param -> { var processedParamName = TbNodeUtils.processPattern(param.name(), msg); @@ -312,9 +312,9 @@ public class TbHttpClient { queryParams.forEach(param -> builder.addParameter(param.name(), param.value())); return builder.build(); } catch (URISyntaxException e) { - throw new IllegalArgumentException(""" - Invalid request URL: '%s'. The URL must be valid and properly encoded. - If URL contains special characters (e.g., spaces), they must be percent-encoded (e.g., '/my file/' should be '/my%%20file/').""".formatted(endpointUrl), e); + throw new IllegalArgumentException( + "Invalid request URL: '" + endpointUrl + "'. The URL must be valid and properly encoded. " + + "If URL contains special characters (e.g., spaces), they must be percent-encoded (e.g., '/my device/' should be '/my%20device/').", e); } } @@ -327,10 +327,10 @@ public class TbHttpClient { * Pre-encoding the URL (e.g., {@code email=user%2Btag@test.com}) doesn't help * because {@code .encode()} will double-encode it to {@code email=user%252Btag@test.com}. *

- * Use {@link #buildEncodedUriNew} with a separate query params map, + * Use {@link #buildEncodedUriNew} with a separate query params list, * where values are encoded exactly once: {@code +} → {@code %2B}, {@code @} → {@code %40}. */ - @Deprecated(since = "4.2.1.2", forRemoval = true) // fixme: remove in major 5.0 + @Deprecated(since = "4.2.1.2", forRemoval = true) private URI buildEncodedUriLegacy(String endpointUrl) { return UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri(); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index cd12eb5211..d319d15157 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -15,10 +15,8 @@ */ package org.thingsboard.rule.engine.rest; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.validation.Valid; -import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.http.HttpHeaders; @@ -37,7 +35,6 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration queryParams; private Map headers; private boolean useSimpleClientHttpFactory; @@ -55,31 +52,12 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) - .isInstanceOf(TbNodeException.class) - .hasRootCauseInstanceOf(DataValidationException.class) - .hasRootCauseMessage("'" + ruleNode.getName() + "' node configuration is invalid: query parameters must be null if old encoding is used") - .matches(e -> ((TbNodeException) e).isUnrecoverable()); - } - - @Test - public void shouldNotAllowNullQueryParamsIfNewEncodingIsUsed() { - // GIVEN - var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); - config.setUseNewEncoding(true); - config.setQueryParams(null); + var queryParams = new ArrayList(); + queryParams.add(new QueryParam("key", "value")); + queryParams.add(null); + config.setQueryParams(queryParams); // WHEN-THEN assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) .isInstanceOf(TbNodeException.class) - .hasRootCauseInstanceOf(DataValidationException.class) - .hasRootCauseMessage("'" + ruleNode.getName() + "' node configuration is invalid: query parameters must be non-null if new encoding is used") - .matches(e -> ((TbNodeException) e).isUnrecoverable()); + .matches(e -> ((TbNodeException) e).isUnrecoverable()) + .rootCause() + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("must not be null"); } @Test @@ -187,7 +174,6 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { var defaultConfig = new TbRestApiCallNodeConfiguration().defaultConfiguration(); // THEN - assertTrue(defaultConfig.isUseNewEncoding()); assertEquals(Collections.emptyList(), defaultConfig.getQueryParams()); } @@ -224,7 +210,6 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { var config = JacksonUtil.fromString(configJson, TbRestApiCallNodeConfiguration.class); // THEN - assertFalse(config.isUseNewEncoding()); assertNull(config.getQueryParams()); } From bc322a966d30997b38b9fd9d836d9b52e501151e Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 9 Jan 2026 12:23:48 +0200 Subject: [PATCH 013/313] REST API call node: cleanup properties in node config --- .../rule/engine/rest/TbHttpClient.java | 4 -- .../rule/engine/rest/TbRestApiCallNode.java | 20 ++++++- .../rest/TbRestApiCallNodeConfiguration.java | 7 +-- .../rule/engine/rest/TbHttpClientTest.java | 1 - .../engine/rest/TbRestApiCallNodeTest.java | 55 ++++++++++++++++++- 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index e9476157aa..05f5d547a8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -130,10 +130,6 @@ public class TbHttpClient { SslContext sslContext = config.getCredentials().initSslContext(); httpClient = httpClient.secure(t -> t.sslContext(sslContext)); } - } else if (config.isUseSimpleClientHttpFactory()) { - if (CredentialsType.CERT_PEM == config.getCredentials().getType()) { - throw new TbNodeException("Simple HTTP Factory does not support CERT PEM credentials!"); - } } else { SslContext sslContext = config.getCredentials().initSslContext(); httpClient = httpClient.secure(t -> t.sslContext(sslContext)); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index 3e9b034693..bccbc9a08f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -28,7 +28,9 @@ import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.exception.DataValidationException; +import java.util.Iterator; import java.util.List; +import java.util.Set; import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; @@ -36,7 +38,7 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie type = ComponentType.EXTERNAL, name = "rest api call", configClazz = TbRestApiCallNodeConfiguration.class, - version = 3, + version = 4, nodeDescription = "Invoke REST API calls to external REST server", nodeDetails = "Will invoke REST API call GET | POST | PUT | DELETE to external REST server. " + "Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." + @@ -108,6 +110,22 @@ public class TbRestApiCallNode extends TbAbstractExternalNode { ((ObjectNode) oldConfiguration).put(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB, 256); } break; + case 3: + Set knownProperties = Set.of( + "restEndpointUrlPattern", "requestMethod", "headers", + "readTimeoutMs", "maxParallelRequestsCount", "parseToPlainText", + "enableProxy", "useSystemProxyProperties", "proxyHost", "proxyPort", + "proxyUser", "proxyPassword", "credentials", "ignoreRequestBody", + "maxInMemoryBufferSizeInKb" + ); + Iterator fieldNames = oldConfiguration.fieldNames(); + while (fieldNames.hasNext()) { + if (!knownProperties.contains(fieldNames.next())) { + hasChanges = true; + fieldNames.remove(); + } + } + break; default: break; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index d319d15157..fbff14f640 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.rest; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -29,15 +28,15 @@ import java.util.Collections; import java.util.List; import java.util.Map; -@JsonIgnoreProperties(ignoreUnknown = true) @Data public class TbRestApiCallNodeConfiguration implements NodeConfiguration { private String restEndpointUrlPattern; private String requestMethod; + // null for legacy configs - triggers different URL encoding logic in TbHttpClient to preserver backward compatibility + // all new/modified configs since introduction of this property are forced to have non-null value by UI (and use new encoding logic) private List<@NotNull @Valid QueryParam> queryParams; private Map headers; - private boolean useSimpleClientHttpFactory; private int readTimeoutMs; private int maxParallelRequestsCount; private boolean parseToPlainText; @@ -47,7 +46,6 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration Date: Fri, 9 Jan 2026 14:34:25 +0200 Subject: [PATCH 014/313] Move REST API call rule node to new UI --- .../common/credentials-config.component.html | 114 ++++----- .../common/kv-map-config-old.component.html | 78 +++--- .../common/kv-map-config-old.component.ts | 4 + .../rest-api-call-config.component.html | 227 ++++++++++-------- .../rest-api-call-config.component.scss | 30 +++ .../rest-api-call-config.component.ts | 28 ++- .../assets/locale/locale.constant-en_US.json | 9 + 7 files changed, 290 insertions(+), 200 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html index 59bfa7ce9f..9a42bfda14 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html @@ -15,8 +15,8 @@ limitations under the License. --> -

- +
+ rule-node-config.credentials @@ -24,7 +24,7 @@ - + rule-node-config.credentials-type -
- - - - - rule-node-config.username - - - {{ 'rule-node-config.username-required' | translate }} - - - - rule-node-config.password - - - - {{ 'rule-node-config.password-required' | translate }} - - - - -
{{ 'rule-node-config.credentials-pem-hint' | translate }}
- - - - - - - - rule-node-config.private-key-password - - - -
-
+ @if (credentialsConfigFormGroup.get('type').value !== 'anonymous') { +
+ + + rule-node-config.username + + + {{ 'rule-node-config.username-required' | translate }} + + + + rule-node-config.password + + + + {{ 'rule-node-config.password-required' | translate }} + + + + +
{{ 'rule-node-config.credentials-pem-hint' | translate }}
+ + + + + + + + rule-node-config.private-key-password + + + +
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html index df0d61fead..c3a1981ebc 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html @@ -16,40 +16,52 @@ -->
-
- {{ keyText | translate }} - {{ valText | translate }} - -
-
-
- - - - {{ keyRequiredText | translate }} - - - - - - {{ valRequiredText | translate }} - - - + @if (description) { +
+ } + @if (keyValsFormArray().controls.length || !emptyText) { +
+ {{ keyText | translate }} + {{ valText | translate }} +
-
+ } +
+ @for (keyValControl of keyValsFormArray().controls; track $index) { +
+ + + + {{ keyRequiredText | translate }} + + + + + + {{ valRequiredText | translate }} + + + +
+ } @empty { + @if (emptyText) { +
{{emptyText | translate}}
+ } + } + @if (hintText) { +
+ }
- - rule-node-config.endpoint-url-pattern - - - {{ 'rule-node-config.endpoint-url-pattern-required' | translate }} - - rule-node-config.general-pattern-hint - +
+
+
Request URL
+ + rule-node-config.endpoint-url-pattern + + + {{ 'rule-node-config.endpoint-url-pattern-required' | translate }} + + rule-node-config.url-hint + + + + {{ 'rule-node-config.query-params' | translate }} +
+
+ + > + + +
+
rule-node-config.request-method @@ -32,99 +51,113 @@ - - {{ 'rule-node-config.enable-proxy' | translate }} - - - {{ 'rule-node-config.use-simple-client-http-factory' | translate }} - - - {{ 'rule-node-config.parse-to-plain-text' | translate }} - -
rule-node-config.parse-to-plain-text-hint
- - {{ 'rule-node-config.ignore-request-body' | translate }} - -
- - {{ 'rule-node-config.use-system-proxy-properties' | translate }} - -
-
- - rule-node-config.proxy-scheme - - - {{ proxyScheme }} - - - - - rule-node-config.proxy-host - - - {{ 'rule-node-config.proxy-host-required' | translate }} +
+ + + {{ 'rule-node-config.headers' | translate }} + + + + + + +
+ +
+ + + {{ 'rule-node-config.advanced-settings' | translate }} + + +
+ + {{ 'rule-node-config.enable-proxy' | translate }} + +
+ + {{ 'rule-node-config.use-system-proxy-properties' | translate }} + +
+
+ + rule-node-config.proxy-host + + + {{ 'rule-node-config.proxy-host-required' | translate }} + + + + rule-node-config.proxy-port + + + {{ 'rule-node-config.proxy-port-required' | translate }} + + + {{ 'rule-node-config.proxy-port-range' | translate }} + + +
+ + rule-node-config.proxy-user + + + + rule-node-config.proxy-password + + + +
+
+
+
+ +
+ {{ 'rule-node-config.parse-to-plain-text' | translate }} +
+
+
+
+ + {{ 'rule-node-config.ignore-request-body' | translate }} + +
+ + rule-node-config.read-timeout + + rule-node-config.read-timeout-hint + + {{ 'rule-node-config.int-range' | translate }} - - rule-node-config.proxy-port - - - {{ 'rule-node-config.proxy-port-required' | translate }} + + rule-node-config.max-parallel-requests-count + + rule-node-config.max-parallel-requests-count-hint + + {{ 'rule-node-config.int-range' | translate }} + + + rule-node-config.max-response-size + + rule-node-config.max-response-size-hint - {{ 'rule-node-config.proxy-port-range' | translate }} + *ngIf="restApiCallConfigForm.get('maxInMemoryBufferSizeInKb').hasError('min') || restApiCallConfigForm.get('maxInMemoryBufferSizeInKb').hasError('max')"> + {{ 'rule-node-config.memory-buffer-size-range' | translate: { max: MemoryBufferSizeInKbLimit } }} -
- - rule-node-config.proxy-user - - - - rule-node-config.proxy-password - - -
+ +
- - rule-node-config.read-timeout - - rule-node-config.read-timeout-hint - - {{ 'rule-node-config.int-range' | translate }} - - - - rule-node-config.max-parallel-requests-count - - rule-node-config.max-parallel-requests-count-hint - - {{ 'rule-node-config.int-range' | translate }} - - - - rule-node-config.max-response-size - - rule-node-config.max-response-size-hint - - {{ 'rule-node-config.memory-buffer-size-range' | translate: { max: MemoryBufferSizeInKbLimit } }} - - - -
- - -
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss new file mode 100644 index 0000000000..79b42e8eaa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss @@ -0,0 +1,30 @@ +/** + * 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 ::ng-deep { + .mat-expansion-panel-content { + .mat-expansion-panel-body { + .tb-kv-map-config { + margin-bottom: 0; + .body { + .mdc-icon-button { + margin-bottom: 22px; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts index 483b6318d6..d08b97f7de 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts @@ -18,21 +18,24 @@ import { Component } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; import { HttpRequestType, IntLimit } from '../rule-node-config.models'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-external-node-rest-api-call-config', templateUrl: './rest-api-call-config.component.html', - styleUrls: [] + styleUrls: ['./rest-api-call-config.component.scss'] }) export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { restApiCallConfigForm: UntypedFormGroup; - readonly proxySchemes: string[] = ['http', 'https']; readonly httpRequestTypes = Object.keys(HttpRequestType); readonly MemoryBufferSizeInKbLimit = 25000; readonly IntLimit = IntLimit; + isHeadersExpanded = false; + isQueryParamsExpanded = false; + constructor(private fb: UntypedFormBuilder) { super(); } @@ -45,12 +48,10 @@ export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { this.restApiCallConfigForm = this.fb.group({ restEndpointUrlPattern: [configuration ? configuration.restEndpointUrlPattern : null, [Validators.required]], requestMethod: [configuration ? configuration.requestMethod : null, [Validators.required]], - useSimpleClientHttpFactory: [configuration ? configuration.useSimpleClientHttpFactory : false, []], parseToPlainText: [configuration ? configuration.parseToPlainText : false, []], ignoreRequestBody: [configuration ? configuration.ignoreRequestBody : false, []], enableProxy: [configuration ? configuration.enableProxy : false, []], useSystemProxyProperties: [configuration ? configuration.enableProxy : false, []], - proxyScheme: [configuration ? configuration.proxyHost : null, []], proxyHost: [configuration ? configuration.proxyHost : null, []], proxyPort: [configuration ? configuration.proxyPort : null, []], proxyUser: [configuration ? configuration.proxyUser :null, []], @@ -59,16 +60,22 @@ export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { maxParallelRequestsCount: [configuration ? configuration.maxParallelRequestsCount : null, [Validators.min(0), Validators.max(IntLimit)]], headers: [configuration ? configuration.headers : null, []], credentials: [configuration ? configuration.credentials : null, []], - maxInMemoryBufferSizeInKb: [configuration ? configuration.maxInMemoryBufferSizeInKb : null, [Validators.min(1), Validators.max(this.MemoryBufferSizeInKbLimit)]] + maxInMemoryBufferSizeInKb: [configuration ? configuration.maxInMemoryBufferSizeInKb : null, [Validators.min(1), Validators.max(this.MemoryBufferSizeInKbLimit)]], + queryParams: [configuration ? configuration.queryParams : [], []], }); } protected validatorTriggers(): string[] { - return ['useSimpleClientHttpFactory', 'enableProxy', 'useSystemProxyProperties']; + return ['enableProxy', 'useSystemProxyProperties']; + } + + protected prepareInputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + this.isQueryParamsExpanded = configuration && isDefinedAndNotNull(configuration.queryParams) && !!Object.keys(configuration.queryParams).length; + this.isHeadersExpanded = configuration && isDefinedAndNotNull(configuration.headers) && !!Object.keys(configuration.headers).length; + return super.prepareInputConfig(configuration); } protected updateValidators(emitEvent: boolean) { - const useSimpleClientHttpFactory: boolean = this.restApiCallConfigForm.get('useSimpleClientHttpFactory').value; const enableProxy: boolean = this.restApiCallConfigForm.get('enableProxy').value; const useSystemProxyProperties: boolean = this.restApiCallConfigForm.get('useSystemProxyProperties').value; @@ -79,12 +86,7 @@ export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { } else { this.restApiCallConfigForm.get('proxyHost').setValidators([]); this.restApiCallConfigForm.get('proxyPort').setValidators([]); - - if (useSimpleClientHttpFactory) { - this.restApiCallConfigForm.get('readTimeoutMs').setValidators([]); - } else { - this.restApiCallConfigForm.get('readTimeoutMs').setValidators([Validators.min(0), Validators.max(IntLimit)]); - } + this.restApiCallConfigForm.get('readTimeoutMs').setValidators([Validators.min(0), Validators.max(IntLimit)]); } this.restApiCallConfigForm.get('readTimeoutMs').updateValueAndValidity({emitEvent}); 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 54ea5737f7..5e30e42e97 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5404,6 +5404,15 @@ "tell-failure-aws-lambda-hint": "Rule node forces failure of message processing if AWS Lambda function execution raises exception.", "basic-mode": "Basic", "advanced-mode": "Advanced", + "url-hint" : "Enter the request URL. Ensure special characters are percent-encoded.", + "query-params": "Query parameters", + "query-params-hint": "Additional query parameters to include in the request URL. Both names and values support templatization and are strictly (RFC 3986) encoded after template resolution.", + "query-params-empty": "No query parameters configured", + "query-params-name": "Name", + "query-params-value": "Value", + "query-params-name-required": "Name is required", + "query-params-value-required": "Value is required", + "headers-empty": "No headers configured", "save-time-series": { "processing-settings": "Processing settings", "processing-settings-hint": "Define how incoming messages are processed. Basic processing settings allow you to select preconfigured strategies, while Advanced settings allow you to select individual processing strategies for each action.", From 44a283a5a2455330624e6ca0579a6ec1b0175ff9 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 9 Jan 2026 18:00:58 +0200 Subject: [PATCH 015/313] Translation cleanup --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 -- 1 file changed, 2 deletions(-) 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 5e30e42e97..419eaf50ac 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4964,7 +4964,6 @@ "endpoint-url-pattern": "Endpoint URL pattern", "endpoint-url-pattern-required": "Endpoint URL pattern is required", "request-method": "Request method", - "use-simple-client-http-factory": "Use simple client HTTP factory", "ignore-request-body": "Without request body", "parse-to-plain-text": "Parse to plain text", "parse-to-plain-text-hint": "If selected, request body message payload will be transformed from JSON string to plain text, e.g. msg = \"Hello,\\t\"world\"\" will be parsed to Hello, \"world\"", @@ -5120,7 +5119,6 @@ "proxy-port-range": "Proxy port should be in a range from 1 to 65535.", "proxy-user": "Proxy user", "proxy-password": "Proxy password", - "proxy-scheme": "Proxy scheme", "numbers-to-template": "Phone Numbers To Template", "numbers-to-template-required": "Phone Numbers To Template is required", "numbers-to-template-hint": "Comma separated Phone Numbers, use ${metadataKey} for value from metadata, $[messageKey] for value from message body", From af3ef1d5cecb7859a0f0fc3349071471b7b9866e Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 12 Jan 2026 13:52:09 +0200 Subject: [PATCH 016/313] Fixed set config logic --- .../rule-node/external/rest-api-call-config.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts index d08b97f7de..42685f2ca7 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts @@ -75,6 +75,11 @@ export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { return super.prepareInputConfig(configuration); } + protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + configuration.queryParams = isDefinedAndNotNull(configuration.queryParams) ? configuration.queryParams : []; + return super.prepareOutputConfig(configuration); + } + protected updateValidators(emitEvent: boolean) { const enableProxy: boolean = this.restApiCallConfigForm.get('enableProxy').value; const useSystemProxyProperties: boolean = this.restApiCallConfigForm.get('useSystemProxyProperties').value; From 49c34ebe91f20fa65fed267f9509f15b783c4156 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 Jan 2026 17:33:12 +0200 Subject: [PATCH 017/313] Use min value from tenant profile for scheduled update interval when not provided --- ...SupportedCalculatedFieldConfiguration.java | 8 +- ...gregationCalculatedFieldConfiguration.java | 2 +- ...eofencingCalculatedFieldConfiguration.java | 5 +- ...ncingCalculatedFieldConfigurationTest.java | 14 ++ .../dao/cf/BaseCalculatedFieldService.java | 14 ++ .../service/CalculatedFieldServiceTest.java | 169 +++++++++++++++++- 6 files changed, 201 insertions(+), 11 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index f5b45c6d5c..f78ff72af0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -22,14 +22,14 @@ public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends Ca boolean isScheduledUpdateEnabled(); @PositiveOrZero - int getScheduledUpdateInterval(); + Integer getScheduledUpdateInterval(); - void setScheduledUpdateInterval(int interval); + void setScheduledUpdateInterval(Integer interval); default void validate(long minAllowedScheduledUpdateInterval) { if (getScheduledUpdateInterval() < minAllowedScheduledUpdateInterval) { - throw new IllegalArgumentException("Scheduled update interval is less than configured " + - "minimum allowed interval in tenant profile: " + minAllowedScheduledUpdateInterval); + throw new IllegalArgumentException("Scheduled update interval (" + getScheduledUpdateInterval() + + " seconds) is less than minimum allowed interval in tenant profile: " + minAllowedScheduledUpdateInterval + " seconds"); } } } 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 3b0df7ed82..98b4b691e3 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 @@ -44,7 +44,7 @@ public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements A private Output output; private boolean useLatestTs; - private int scheduledUpdateInterval; + private Integer scheduledUpdateInterval; @Override public CalculatedFieldType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java index 2d1abd0024..c85604d2ab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -46,7 +46,7 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal private Map zoneGroups; private boolean scheduledUpdateEnabled; - private int scheduledUpdateInterval; + private Integer scheduledUpdateInterval; private Output output; @@ -79,6 +79,9 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal @Override public void validate() { + if (scheduledUpdateEnabled && scheduledUpdateInterval == null) { + throw new IllegalArgumentException("Refresh interval is required when periodic zone group refresh is enabled."); + } zoneGroups.forEach((key, value) -> value.validate(key)); } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java index 2f391435b2..2c68bcd8c5 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -28,6 +28,7 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; @@ -103,4 +104,17 @@ public class GeofencingCalculatedFieldConfigurationTest { assertThat(allowedZonesArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("perimeter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); } + @Test + void validateShouldThrowWhenScheduledUpdateEnabledButIntervalNotSet() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(mock(EntityCoordinates.class)); + cfg.setZoneGroups(Map.of("zone", mock(ZoneGroupConfiguration.class))); + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(null); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Refresh interval is required when periodic zone group refresh is enabled."); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 00c5ec372e..075e2d8a9d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -26,18 +26,21 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; 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.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.validator.CalculatedFieldDataValidator; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; import java.util.EnumSet; import java.util.List; @@ -62,6 +65,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements private final EntityService entityService; private final CalculatedFieldDao calculatedFieldDao; private final CalculatedFieldDataValidator calculatedFieldDataValidator; + private final ApiLimitService apiLimitService; @Override public CalculatedField save(CalculatedField calculatedField) { @@ -70,6 +74,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements @Override public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { + setConfigurationDefaults(calculatedField); CalculatedField oldCalculatedField = null; if (doValidate) { oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); @@ -79,6 +84,15 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return doSave(calculatedField, oldCalculatedField); } + private void setConfigurationDefaults(CalculatedField calculatedField) { + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config + && config.getScheduledUpdateInterval() == null) { + int minScheduledUpdateInterval = (int) apiLimitService.getLimit( + calculatedField.getTenantId(), DefaultTenantProfileConfiguration::getMinAllowedScheduledUpdateIntervalInSecForCF + ); + config.setScheduledUpdateInterval(minScheduledUpdateInterval); + } + } private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 97e06ed879..afb380c5be 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.service; +import org.apache.commons.lang3.RandomUtils; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.Device; @@ -27,6 +28,10 @@ import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; @@ -36,8 +41,8 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.List; @@ -107,11 +112,11 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { int min = tbTenantProfileCache.get(tenantId) .getDefaultProfileConfiguration() .getMinAllowedScheduledUpdateIntervalInSecForCF(); - int valueFromConfig = min - 10; // Enable scheduling with an interval below tenant min cfg.setScheduledUpdateEnabled(true); - cfg.setScheduledUpdateInterval(valueFromConfig); + int invalidInterval = RandomUtils.insecure().randomInt(1, min); + cfg.setScheduledUpdateInterval(invalidInterval); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); @@ -125,8 +130,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThatThrownBy(() -> calculatedFieldService.save(cf)) .isInstanceOf(DataValidationException.class) .hasCauseInstanceOf(IllegalArgumentException.class) - .hasMessageStartingWith("Scheduled update interval is less than configured " + - "minimum allowed interval in tenant profile: "); + .hasMessage("Scheduled update interval (" + invalidInterval + + " seconds) is less than minimum allowed interval in tenant profile: " + min + " seconds"); } @Test @@ -253,6 +258,160 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThat(calculatedFieldService.findById(tenantId, savedCalculatedField.getId())).isNull(); } + @Test + public void testSaveRelatedEntitiesAggregationCF_shouldUseMinScheduledUpdateIntervalFromTenantProfileWhenNotSet() { + // GIVEN + var device = createTestDevice(); + + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temp", argument)); + + var metric = new AggMetric(); + metric.setFunction(AggFunction.AVG); + metric.setInput(new AggKeyInput("temp")); + cfg.setMetrics(Map.of("avgTemp", metric)); + + var output = new TimeSeriesOutput(); + output.setName("avgTemperature"); + cfg.setOutput(output); + + int minDeduplicationInterval = (int) tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedDeduplicationIntervalInSecForCF(); + cfg.setDeduplicationIntervalInSec(minDeduplicationInterval); + + // Do NOT set scheduledUpdateInterval - it should default to tenant profile min value + + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + cf.setName("Related Entities Aggregation CF - default scheduled interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN + CalculatedField saved = calculatedFieldService.save(cf); + + // THEN + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(RelatedEntitiesAggregationCalculatedFieldConfiguration.class); + + var savedConfig = (RelatedEntitiesAggregationCalculatedFieldConfiguration) saved.getConfiguration(); + int expectedMinScheduledUpdateInterval = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + + assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(expectedMinScheduledUpdateInterval); + + calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + } + + @Test + public void testSaveRelatedEntitiesAggregationCF_shouldThrowWhenScheduledUpdateIntervalLessThanMinAllowed() { + // GIVEN + var device = createTestDevice(); + + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temp", argument)); + + var metric = new AggMetric(); + metric.setFunction(AggFunction.AVG); + metric.setInput(new AggKeyInput("temp")); + cfg.setMetrics(Map.of("avgTemp", metric)); + + var output = new TimeSeriesOutput(); + output.setName("avgTemperature"); + cfg.setOutput(output); + + int minDeduplicationInterval = (int) tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedDeduplicationIntervalInSecForCF(); + cfg.setDeduplicationIntervalInSec(minDeduplicationInterval); + + int minScheduledUpdateInterval = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int invalidInterval = RandomUtils.insecure().randomInt(1, minScheduledUpdateInterval); + cfg.setScheduledUpdateInterval(invalidInterval); + + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + cf.setName("Related Entities Aggregation CF - invalid scheduled interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN-THEN + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update interval (" + invalidInterval + + " seconds) is less than minimum allowed interval in tenant profile: " + minScheduledUpdateInterval + " seconds"); + } + + @Test + public void testSaveRelatedEntitiesAggregationCF_shouldAcceptValidScheduledUpdateInterval() { + // GIVEN + var device = createTestDevice(); + + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temp", argument)); + + var metric = new AggMetric(); + metric.setFunction(AggFunction.AVG); + metric.setInput(new AggKeyInput("temp")); + cfg.setMetrics(Map.of("avgTemp", metric)); + + var output = new TimeSeriesOutput(); + output.setName("avgTemperature"); + cfg.setOutput(output); + + int minDeduplicationInterval = (int) tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedDeduplicationIntervalInSecForCF(); + cfg.setDeduplicationIntervalInSec(minDeduplicationInterval); + + int minScheduledUpdateInterval = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int customScheduledUpdateInterval = minScheduledUpdateInterval + 100; + cfg.setScheduledUpdateInterval(customScheduledUpdateInterval); + + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + cf.setName("Related Entities Aggregation CF - valid scheduled interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN + CalculatedField saved = calculatedFieldService.save(cf); + + // THEN + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(RelatedEntitiesAggregationCalculatedFieldConfiguration.class); + + var savedConfig = (RelatedEntitiesAggregationCalculatedFieldConfiguration) saved.getConfiguration(); + assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(customScheduledUpdateInterval); + + calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + } + private CalculatedField saveValidCalculatedField() { Device device = createTestDevice(); CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId()); From 231b2fe8bc178f075ff66b5b3d05b8e74f2f88d6 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 Jan 2026 18:07:35 +0200 Subject: [PATCH 018/313] Add validation for min scheduled update interval in tenant profile --- .../DefaultTenantProfileConfiguration.java | 2 + .../TenantProfileDataValidatorTest.java | 56 +++++++++++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index a04816365b..297f92cffb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.tenant.profile; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -173,6 +174,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura @Schema(example = "10") private long maxArgumentsPerCF = 10; @Schema(example = "10") + @PositiveOrZero private int minAllowedScheduledUpdateIntervalInSecForCF = 10; @Builder.Default @Schema(example = "2") diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java index 170b452fee..373344cb6d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java @@ -16,29 +16,35 @@ package org.thingsboard.server.dao.service.validator; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.tenant.TenantProfileDao; import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.exception.DataValidationException; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.verify; @SpringBootTest(classes = TenantProfileDataValidator.class) class TenantProfileDataValidatorTest { - @MockBean + @MockitoBean TenantProfileDao tenantProfileDao; - @MockBean + @MockitoBean TenantProfileService tenantProfileService; - @SpyBean + @MockitoSpyBean TenantProfileDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); @Test @@ -53,4 +59,44 @@ class TenantProfileDataValidatorTest { verify(validator).validateString("Tenant profile name", tenantProfile.getName()); } + @ParameterizedTest + @ValueSource(ints = {-1, -100, Integer.MIN_VALUE}) + void minAllowedScheduledUpdateIntervalInSecForCF_shouldRejectNegativeValues(int value) { + // GIVEN + var config = new DefaultTenantProfileConfiguration(); + config.setMinAllowedScheduledUpdateIntervalInSecForCF(value); + + var tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(config); + + var tenantProfile = new TenantProfile(); + tenantProfile.setName("Test"); + tenantProfile.setProfileData(tenantProfileData); + + // WHEN/THEN + assertThatThrownBy(() -> validator.validate(tenantProfile, __ -> TenantId.SYS_TENANT_ID)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("minAllowedScheduledUpdateIntervalInSecForCF") + .hasMessageContaining("must be greater than or equal to 0"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 60, Integer.MAX_VALUE}) + void minAllowedScheduledUpdateIntervalInSecForCF_shouldAcceptValidValues(int value) { + // GIVEN + var config = new DefaultTenantProfileConfiguration(); + config.setMinAllowedScheduledUpdateIntervalInSecForCF(value); + + var tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(config); + + var tenantProfile = new TenantProfile(); + tenantProfile.setName("Test"); + tenantProfile.setProfileData(tenantProfileData); + + // WHEN/THEN + assertThatCode(() -> validator.validate(tenantProfile, __ -> TenantId.SYS_TENANT_ID)) + .doesNotThrowAnyException(); + } + } From b688ca1dcdac48a4f04daad9249a148dfc24242b Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 16 Jan 2026 09:32:52 +0200 Subject: [PATCH 019/313] Add tests for zero min scheduled update interval --- ...ortedCalculatedFieldConfigurationTest.java | 4 +- .../dao/service/AbstractServiceTest.java | 4 +- .../service/CalculatedFieldServiceTest.java | 69 ++++++++++++++++--- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java index 23e0f1add3..a2df32b483 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java @@ -47,8 +47,8 @@ public class ScheduledUpdateSupportedCalculatedFieldConfigurationTest { assertThatThrownBy(() -> cfg.validate(minAllowedInterval)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Scheduled update interval is less than configured " + - "minimum allowed interval in tenant profile: " + minAllowedInterval); + .hasMessage("Scheduled update interval (1 seconds) is less than " + + "minimum allowed interval in tenant profile: " + minAllowedInterval + " seconds"); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 9a467071ef..5ee9af6292 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -93,11 +93,13 @@ public abstract class AbstractServiceTest { @Autowired protected EntityServiceRegistry entityServiceRegistry; + protected Tenant tenant; protected TenantId tenantId; @Before public void beforeAbstractService() { - tenantId = createTenant().getId(); + tenant = createTenant(); + tenantId = tenant.getId(); } @After diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index afb380c5be..b5bc51537c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.RandomUtils; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; @@ -42,6 +43,7 @@ import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; @@ -62,6 +64,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { private DeviceService deviceService; @Autowired private TbTenantProfileCache tbTenantProfileCache; + @Autowired + private TenantProfileService tenantProfileService; @Test public void testSaveCalculatedField() { @@ -85,8 +89,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); - - calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId()); } @Test @@ -224,8 +226,63 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); assertThat(savedInterval).isEqualTo(valueFromConfig); + } - calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + @Test + public void testSaveGeofencingCalculatedField_shouldAcceptZeroScheduledUpdateIntervalWhenTenantProfileAllows() { + // GIVEN + var device = createTestDevice(); + + // Store original value and update tenant profile to allow 0 as min scheduled update interval + TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId()); + int originalMinScheduledUpdateInterval = tenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF(); + tenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(0); + tenantProfileService.saveTenantProfile(tenantId, tenantProfile); + tbTenantProfileCache.evict(tenantProfile.getId()); + + try { + // Build a valid Geofencing configuration + var cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + var entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + var zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + // Enable scheduling with interval = 0 + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(0); + + // Create Calculated Field + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF zero scheduled update interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN + CalculatedField saved = calculatedFieldService.save(cf); + + // THEN + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + + var savedConfig = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(0); + } finally { + // Restore original tenant profile value + tenantProfile.getProfileConfiguration().orElseThrow().setMinAllowedScheduledUpdateIntervalInSecForCF(originalMinScheduledUpdateInterval); + tenantProfileService.saveTenantProfile(tenantId, tenantProfile); + tbTenantProfileCache.evict(tenantProfile.getId()); + } } @Test @@ -245,8 +302,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { CalculatedField fetchedCalculatedField = calculatedFieldService.findById(tenantId, savedCalculatedField.getId()); assertThat(fetchedCalculatedField).isEqualTo(savedCalculatedField); - - calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId()); } @Test @@ -307,8 +362,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { .getMinAllowedScheduledUpdateIntervalInSecForCF(); assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(expectedMinScheduledUpdateInterval); - - calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); } @Test @@ -408,8 +461,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { var savedConfig = (RelatedEntitiesAggregationCalculatedFieldConfiguration) saved.getConfiguration(); assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(customScheduledUpdateInterval); - - calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); } private CalculatedField saveValidCalculatedField() { From 1bd9b8d287a237ab35397aa329b082556424382c Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 16 Jan 2026 15:01:18 +0200 Subject: [PATCH 020/313] Added component for query params --- .../common/common-rule-node-config.module.ts | 7 +- .../common/kv-list-config.component.ts | 89 +++++++++++++++++++ .../common/kv-map-config-old.component.ts | 6 +- .../rest-api-call-config.component.html | 9 +- .../assets/locale/locale.constant-en_US.json | 1 + 5 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts index c7d8ce2723..e21f0aa689 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts @@ -34,6 +34,7 @@ import { SelectAttributesComponent } from './select-attributes.component'; import { AlarmStatusSelectComponent } from './alarm-status-select.component'; import { ExampleHintComponent } from './example-hint.component'; import { TimeUnitInputComponent } from './time-unit-input.component'; +import { KvListConfigComponent } from '@home/components/rule-node/common/kv-list-config.component'; @NgModule({ declarations: [ @@ -52,7 +53,8 @@ import { TimeUnitInputComponent } from './time-unit-input.component'; SelectAttributesComponent, AlarmStatusSelectComponent, ExampleHintComponent, - TimeUnitInputComponent + TimeUnitInputComponent, + KvListConfigComponent ], imports: [ CommonModule, @@ -75,7 +77,8 @@ import { TimeUnitInputComponent } from './time-unit-input.component'; SelectAttributesComponent, AlarmStatusSelectComponent, ExampleHintComponent, - TimeUnitInputComponent + TimeUnitInputComponent, + KvListConfigComponent ] }) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts new file mode 100644 index 0000000000..5284b88bce --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts @@ -0,0 +1,89 @@ +/// +/// 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, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { KvMapConfigOldComponent } from '@home/components/rule-node/common/kv-map-config-old.component'; + +@Component({ + selector: 'tb-kv-list-config', + templateUrl: './kv-map-config-old.component.html', + styleUrls: ['./kv-map-config-old.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KvListConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KvListConfigComponent), + multi: true, + } + ] +}) +export class KvListConfigComponent extends KvMapConfigOldComponent implements ControlValueAccessor, OnInit, Validator{ + + @Input() keyProperty: string = 'key'; + + override writeValue(kvList: any): void { + const keyValsControls: Array = []; + if (Array.isArray(kvList) && kvList.length > 0) { + for (const property of kvList) { + keyValsControls.push(this.fb.group({ + key: [property[this.keyProperty], [Validators.required]], + value: [property.value, [Validators.required]] + })) + } + } + this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls), {emitEvent: false}); + } + + public override validate() { + const kvList: { key: string; value: string }[] = this.kvListFormGroup.get('keyVals').value; + if (!kvList.length && this.required) { + return { + kvMapRequired: true + }; + } + if (!this.kvListFormGroup.valid) { + return { + kvFieldsRequired: true + }; + } + return null; + } + + protected override updateModel() { + const kvList: { key: string; value: string }[] = this.kvListFormGroup.get('keyVals').value; + if (this.required && !kvList.length || !this.kvListFormGroup.valid) { + this.propagateChange(null); + } else { + const result = kvList.map(entry => ({ + [this.keyProperty]: entry.key, + value: entry.value + })); + this.propagateChange(result); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.ts index bd56212d4f..69fe1820eb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.ts @@ -86,11 +86,11 @@ export class KvMapConfigOldComponent extends PageComponent implements ControlVal ngControl: NgControl; - private propagateChange = null; + public propagateChange = null; constructor(public translate: TranslateService, private injector: Injector, - private fb: FormBuilder, + protected fb: FormBuilder, private destroyRef: DestroyRef) { super(); } @@ -182,7 +182,7 @@ export class KvMapConfigOldComponent extends PageComponent implements ControlVal return null; } - private updateModel() { + protected updateModel() { const kvList: { key: string; value: string }[] = this.kvListFormGroup.get('keyVals').value; if (this.required && !kvList.length || !this.kvListFormGroup.valid) { this.propagateChange(null); diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html index b265b997ac..7a30723f9e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html @@ -19,7 +19,7 @@
Request URL
- rule-node-config.endpoint-url-pattern + rule-node-config.url {{ 'rule-node-config.endpoint-url-pattern-required' | translate }} @@ -32,14 +32,15 @@
- > - + emptyText="rule-node-config.query-params-empty" + keyProperty="name"> +
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 419eaf50ac..2ae1b4ff48 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5411,6 +5411,7 @@ "query-params-name-required": "Name is required", "query-params-value-required": "Value is required", "headers-empty": "No headers configured", + "url": "URL", "save-time-series": { "processing-settings": "Processing settings", "processing-settings-hint": "Define how incoming messages are processed. Basic processing settings allow you to select preconfigured strategies, while Advanced settings allow you to select individual processing strategies for each action.", From 07fd49c080944acdea5888b75d1cccb94eb72d41 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 16 Jan 2026 17:35:32 +0200 Subject: [PATCH 021/313] Moved to key name prop in Query params --- .../rule-node/common/kv-list-config.component.ts | 12 +++--------- .../external/rest-api-call-config.component.html | 3 +-- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts index 5284b88bce..fe913815cd 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-list-config.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, OnInit } from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -44,14 +44,12 @@ import { KvMapConfigOldComponent } from '@home/components/rule-node/common/kv-ma }) export class KvListConfigComponent extends KvMapConfigOldComponent implements ControlValueAccessor, OnInit, Validator{ - @Input() keyProperty: string = 'key'; - override writeValue(kvList: any): void { const keyValsControls: Array = []; if (Array.isArray(kvList) && kvList.length > 0) { for (const property of kvList) { keyValsControls.push(this.fb.group({ - key: [property[this.keyProperty], [Validators.required]], + key: [property.key, [Validators.required]], value: [property.value, [Validators.required]] })) } @@ -79,11 +77,7 @@ export class KvListConfigComponent extends KvMapConfigOldComponent implements Co if (this.required && !kvList.length || !this.kvListFormGroup.valid) { this.propagateChange(null); } else { - const result = kvList.map(entry => ({ - [this.keyProperty]: entry.key, - value: entry.value - })); - this.propagateChange(result); + this.propagateChange(kvList); } } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html index 7a30723f9e..2f6840639c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html @@ -38,8 +38,7 @@ keyRequiredText="rule-node-config.query-params-name-required" valText="rule-node-config.query-params-value" valRequiredText="rule-node-config.query-params-value-required" - emptyText="rule-node-config.query-params-empty" - keyProperty="name"> + emptyText="rule-node-config.query-params-empty"> From 38d35f1ca8366aa728d0eefd98c280c0e15df986 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 19 Jan 2026 10:37:00 +0200 Subject: [PATCH 022/313] Fix upgrade script --- .../rule/engine/rest/TbRestApiCallNode.java | 1 - .../rule/engine/rest/TbRestApiCallNodeTest.java | 16 ++++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index bccbc9a08f..86dd5e576e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -109,7 +109,6 @@ public class TbRestApiCallNode extends TbAbstractExternalNode { hasChanges = true; ((ObjectNode) oldConfiguration).put(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB, 256); } - break; case 3: Set knownProperties = Set.of( "restEndpointUrlPattern", "requestMethod", "headers", diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index 011b035e87..c51c044e18 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -345,7 +345,6 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { return Stream.of( - // config for version 2 with upgrade from version 0 Arguments.of(0, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\":\"POST\"," + "\"useSimpleClientHttpFactory\":false,\"ignoreRequestBody\":false,\"enableProxy\":false," + @@ -355,13 +354,12 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { "\"trimQueue\":null,\"maxQueueSize\":null,\"credentials\":{\"type\":\"anonymous\"},\"trimDoubleQuotes\":false}", true, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + - "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + - "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + + "\"parseToPlainText\": false,\"ignoreRequestBody\": false," + + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyHost\": null," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + "\"credentials\": {\"type\": \"anonymous\"}," + "\"maxInMemoryBufferSizeInKb\": 256}"), - // config for version 2 with upgrade from version 1 Arguments.of(1, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + @@ -372,13 +370,12 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { "\"credentials\": {\"type\": \"anonymous\"}}", true, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + - "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + - "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + + "\"parseToPlainText\": false,\"ignoreRequestBody\": false," + + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyHost\": null," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + "\"credentials\": {\"type\": \"anonymous\"}," + "\"maxInMemoryBufferSizeInKb\": 256}"), - // config for version 3 with upgrade from version 2 Arguments.of(2, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + @@ -388,13 +385,12 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { "\"credentials\": {\"type\": \"anonymous\"}}", true, "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + - "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," + - "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + + "\"parseToPlainText\": false,\"ignoreRequestBody\": false," + + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyHost\": null," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + "\"credentials\": {\"type\": \"anonymous\"}," + "\"maxInMemoryBufferSizeInKb\": 256}"), - // config for version 4 with upgrade from version 3 Arguments.of(3, """ { "restEndpointUrlPattern": "http://localhost/api", From 208cbb9566e25d63dc1f9eb46a9aba4fe96381b8 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 19 Jan 2026 10:37:38 +0200 Subject: [PATCH 023/313] Remove version from deprecation annotation --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 05f5d547a8..9fb6e451e8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -326,7 +326,7 @@ public class TbHttpClient { * Use {@link #buildEncodedUriNew} with a separate query params list, * where values are encoded exactly once: {@code +} → {@code %2B}, {@code @} → {@code %40}. */ - @Deprecated(since = "4.2.1.2", forRemoval = true) + @Deprecated(forRemoval = true) private URI buildEncodedUriLegacy(String endpointUrl) { return UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri(); } From e3f1f7f7625f3bdeb0289650fd9ede53a91eace1 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 19 Jan 2026 10:40:25 +0200 Subject: [PATCH 024/313] Fix type --- .../rule/engine/rest/TbRestApiCallNodeConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index fbff14f640..ac4d493819 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -33,7 +33,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration queryParams; private Map headers; From 0f288a137996d59408a840bb3446d0738ef85c83 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 19 Jan 2026 11:15:33 +0200 Subject: [PATCH 025/313] Refactor `QueryParam` into general-purpose `KeyValueEntry` --- .../common/data/util/KeyValueEntry.java | 18 ++++++++++++ .../rule/engine/rest/TbHttpClient.java | 2 +- .../rest/TbRestApiCallNodeConfiguration.java | 29 ++++++++++++++----- .../rule/engine/rest/TbHttpClientTest.java | 11 +++---- .../engine/rest/TbRestApiCallNodeTest.java | 13 +++++---- 5 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/util/KeyValueEntry.java diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/KeyValueEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/KeyValueEntry.java new file mode 100644 index 0000000000..3e758f69aa --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/KeyValueEntry.java @@ -0,0 +1,18 @@ +/** + * 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.util; + +public record KeyValueEntry(K key, V value) {} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 9fb6e451e8..64b4f70b64 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -216,7 +216,7 @@ public class TbHttpClient { if (config.getQueryParams() != null) { processedQueryParams = config.getQueryParams().stream() .map(param -> { - var processedParamName = TbNodeUtils.processPattern(param.name(), msg); + var processedParamName = TbNodeUtils.processPattern(param.key(), msg); var processedParamValue = TbNodeUtils.processPattern(param.value(), msg); return new QueryParam(processedParamName, processedParamValue); }) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index ac4d493819..bbef69ff31 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -15,7 +15,8 @@ */ package org.thingsboard.rule.engine.rest; -import jakarta.validation.Valid; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.http.HttpHeaders; @@ -23,10 +24,12 @@ import org.springframework.http.MediaType; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.credentials.AnonymousCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials; +import org.thingsboard.server.common.data.util.KeyValueEntry; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; @Data public class TbRestApiCallNodeConfiguration implements NodeConfiguration { @@ -35,7 +38,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration queryParams; + private List<@NotNull KeyValueEntry> queryParams; private Map headers; private int readTimeoutMs; private int maxParallelRequestsCount; @@ -50,9 +53,23 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration queryParam : queryParams) { + if (queryParam == null || queryParam.key() == null || queryParam.value() == null) { + return false; + } + } + return true; + } + @Override public TbRestApiCallNodeConfiguration defaultConfiguration() { - TbRestApiCallNodeConfiguration configuration = new TbRestApiCallNodeConfiguration(); + var configuration = new TbRestApiCallNodeConfiguration(); configuration.setRestEndpointUrlPattern("http://localhost/api"); configuration.setRequestMethod("POST"); configuration.setHeaders(Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); @@ -68,11 +85,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration("email", "${userEmail}"), // ${} from metadata + new KeyValueEntry<>("device", "${deviceName}"), // ${} from metadata + new KeyValueEntry<>("${dynamicParam}", "${dynamicValue}"), // ${} in both key and value + new KeyValueEntry<>("temp", "$[temperature]"), // $[] from data + new KeyValueEntry<>("location", "$[sensor.location]") // $[] from nested data )); var metaData = new TbMsgMetaData(); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index c51c044e18..90d4cdc47d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.util.KeyValueEntry; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -123,7 +124,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { public void shouldNotAllowNullQueryParamNames() { // GIVEN var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); - config.setQueryParams(List.of(new QueryParam(null, "value"))); + config.setQueryParams(List.of(new KeyValueEntry<>(null, "value"))); // WHEN-THEN assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) @@ -131,14 +132,14 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { .matches(e -> ((TbNodeException) e).isUnrecoverable()) .rootCause() .isInstanceOf(DataValidationException.class) - .hasMessageContaining("query parameter names must be non-null"); + .hasMessageContaining("query parameter names and values must be non-null"); } @Test public void shouldNotAllowNullQueryParamValues() { // GIVEN var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); - config.setQueryParams(List.of(new QueryParam("key", null))); + config.setQueryParams(List.of(new KeyValueEntry<>("key", null))); // WHEN-THEN assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) @@ -146,7 +147,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { .matches(e -> ((TbNodeException) e).isUnrecoverable()) .rootCause() .isInstanceOf(DataValidationException.class) - .hasMessageContaining("query parameter values must be non-null"); + .hasMessageContaining("query parameter names and values must be non-null"); } @Test @@ -154,8 +155,8 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { // GIVEN var config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); - var queryParams = new ArrayList(); - queryParams.add(new QueryParam("key", "value")); + var queryParams = new ArrayList>(); + queryParams.add(new KeyValueEntry<>("key", "value")); queryParams.add(null); config.setQueryParams(queryParams); From 0ea01d61af763404da95cd0927283488a9a42a79 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 19 Jan 2026 12:45:54 +0200 Subject: [PATCH 026/313] Cleanup remnants of `QueryParam` --- .../rule/engine/rest/QueryParam.java | 23 ---- .../rule/engine/rest/TbHttpClient.java | 11 +- .../rule/engine/rest/TbHttpClientTest.java | 116 +++++++++--------- 3 files changed, 64 insertions(+), 86 deletions(-) delete mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java deleted file mode 100644 index 3eeecc0560..0000000000 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.rule.engine.rest; - -import jakarta.validation.constraints.NotNull; - -record QueryParam( - @NotNull(message = "query parameter names must be non-null") String name, - @NotNull(message = "query parameter values must be non-null") String value -) {} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 177c6811eb..fb0fbd9f35 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -40,6 +40,7 @@ import org.thingsboard.rule.engine.credentials.BasicCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials; import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.util.KeyValueEntry; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import reactor.netty.http.client.HttpClient; @@ -212,13 +213,13 @@ public class TbHttpClient { String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg); - List processedQueryParams; + List> processedQueryParams; if (config.getQueryParams() != null) { processedQueryParams = config.getQueryParams().stream() .map(param -> { var processedParamName = TbNodeUtils.processPattern(param.key(), msg); var processedParamValue = TbNodeUtils.processPattern(param.value(), msg); - return new QueryParam(processedParamName, processedParamValue); + return new KeyValueEntry<>(processedParamName, processedParamValue); }) .toList(); } else { @@ -274,7 +275,7 @@ public class TbHttpClient { return origin; } - public URI buildEncodedUri(String endpointUrl, List queryParams) { + public URI buildEncodedUri(String endpointUrl, List> queryParams) { if (endpointUrl == null) { throw new RuntimeException("Url string cannot be null!"); } @@ -302,10 +303,10 @@ public class TbHttpClient { return uri; } - private URI buildEncodedUriNew(String endpointUrl, List queryParams) { + private URI buildEncodedUriNew(String endpointUrl, List> queryParams) { try { URIBuilder builder = new URIBuilder(endpointUrl); - queryParams.forEach(param -> builder.addParameter(param.name(), param.value())); + queryParams.forEach(param -> builder.addParameter(param.key(), param.value())); return builder.build(); } catch (URISyntaxException e) { throw new IllegalArgumentException( diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java index eb3b9802aa..9abe3d28a9 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java @@ -234,7 +234,7 @@ public class TbHttpClientTest { @ParameterizedTest(name = "{0}") @MethodSource - public void testQueryParamsEncoding(String endpointUrl, List queryParams, String expectedEncodedUrl) { + public void testQueryParamsEncoding(String endpointUrl, List> queryParams, String expectedEncodedUrl) { // GIVEN Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod(); @@ -249,42 +249,42 @@ public class TbHttpClientTest { return Stream.of( Arguments.of( Named.named("ISO 8601 date-time in value", "http://somecompany/api/data/fetch"), - List.of(new QueryParam("ts", "2016-08-01T09:06:06.0+02:00")), + List.of(new KeyValueEntry<>("ts", "2016-08-01T09:06:06.0+02:00")), "http://somecompany/api/data/fetch?ts=2016-08-01T09%3A06%3A06.0%2B02%3A00" ), Arguments.of( Named.named("email with plus sign in value", "http://localhost:8080/api/user/sendActivationMail"), - List.of(new QueryParam("email", "someperson+test1289@thingsboard.io")), + List.of(new KeyValueEntry<>("email", "someperson+test1289@thingsboard.io")), "http://localhost:8080/api/user/sendActivationMail?email=someperson%2Btest1289%40thingsboard.io" ), Arguments.of( Named.named("plus mixed with spaces in value", "http://url/api"), - List.of(new QueryParam("q", "a + b")), + List.of(new KeyValueEntry<>("q", "a + b")), "http://url/api?q=a%20%2B%20b" ), Arguments.of( Named.named("colon in value", "http://url/api"), - List.of(new QueryParam("time", "12:00")), + List.of(new KeyValueEntry<>("time", "12:00")), "http://url/api?time=12%3A00" ), Arguments.of( Named.named("slash in value", "http://url"), - List.of(new QueryParam("ref", "/home/user")), + List.of(new KeyValueEntry<>("ref", "/home/user")), "http://url?ref=%2Fhome%2Fuser" ), Arguments.of( Named.named("comma and semicolon in value", "http://url"), - List.of(new QueryParam("l", "a,b;c")), + List.of(new KeyValueEntry<>("l", "a,b;c")), "http://url?l=a%2Cb%3Bc" ), Arguments.of( Named.named("ampersand and equals in value", "http://url"), - List.of(new QueryParam("q", "key1=value1&key2=value2")), + List.of(new KeyValueEntry<>("q", "key1=value1&key2=value2")), "http://url?q=key1%3Dvalue1%26key2%3Dvalue2" ), Arguments.of( Named.named("JSON in value", "http://url"), - List.of(new QueryParam("json", """ + List.of(new KeyValueEntry<>("json", """ { "string": "hello", "integer": 42, @@ -307,44 +307,44 @@ public class TbHttpClientTest { Arguments.of( Named.named("UTF-8 in query", "http://url/cafes"), List.of( - new QueryParam("nom", "Le Goût Moderne"), - new QueryParam("назва", "У Миколи \uD83D\uDE0B") + new KeyValueEntry<>("nom", "Le Goût Moderne"), + new KeyValueEntry<>("назва", "У Миколи \uD83D\uDE0B") ), "http://url/cafes?nom=Le%20Go%C3%BBt%20Moderne&%D0%BD%D0%B0%D0%B7%D0%B2%D0%B0=%D0%A3%20%D0%9C%D0%B8%D0%BA%D0%BE%D0%BB%D0%B8%20%F0%9F%98%8B" ), Arguments.of( Named.named("empty value", "http://url/empty"), - List.of(new QueryParam("name", "")), + List.of(new KeyValueEntry<>("name", "")), "http://url/empty?name=" ), Arguments.of( Named.named("blank value (spaces)", "http://url/empty"), - List.of(new QueryParam("name", " ")), + List.of(new KeyValueEntry<>("name", " ")), "http://url/empty?name=%20%20" ), Arguments.of( Named.named("empty key", "http://url/empty"), - List.of(new QueryParam("", "value")), + List.of(new KeyValueEntry<>("", "value")), "http://url/empty?=value" ), Arguments.of( Named.named("blank key (spaces)", "http://url/empty"), - List.of(new QueryParam(" ", "value")), + List.of(new KeyValueEntry<>(" ", "value")), "http://url/empty?%20%20=value" ), Arguments.of( Named.named("blank key with value that needs to be encoded", "http://url/empty"), - List.of(new QueryParam(" ", "value1+value2")), + List.of(new KeyValueEntry<>(" ", "value1+value2")), "http://url/empty?%20%20=value1%2Bvalue2" ), Arguments.of( Named.named("blank key and value", "http://url/empty"), - List.of(new QueryParam(" ", " ")), + List.of(new KeyValueEntry<>(" ", " ")), "http://url/empty?%20%20=%20%20" ), Arguments.of( Named.named("fragment with query params", "http://url#frag"), - List.of(new QueryParam("docs", "مستندات عقدة القاعدة\n")), + List.of(new KeyValueEntry<>("docs", "مستندات عقدة القاعدة\n")), "http://url?docs=%D9%85%D8%B3%D8%AA%D9%86%D8%AF%D8%A7%D8%AA%20%D8%B9%D9%82%D8%AF%D8%A9%20%D8%A7%D9%84%D9%82%D8%A7%D8%B9%D8%AF%D8%A9%0A#frag" ), Arguments.of( @@ -355,105 +355,105 @@ public class TbHttpClientTest { Arguments.of( Named.named("multiple query params (ordered)", "http://url/api"), List.of( - new QueryParam("param1", "value1"), - new QueryParam("param2", "value2"), - new QueryParam("param3", "value3") + new KeyValueEntry<>("param1", "value1"), + new KeyValueEntry<>("param2", "value2"), + new KeyValueEntry<>("param3", "value3") ), "http://url/api?param1=value1¶m2=value2¶m3=value3" ), Arguments.of( Named.named("hash (#) in value", "http://url/api"), - List.of(new QueryParam("color", "#ff0000")), + List.of(new KeyValueEntry<>("color", "#ff0000")), "http://url/api?color=%23ff0000" ), Arguments.of( Named.named("question mark (?) in value", "http://url/api"), - List.of(new QueryParam("query", "what?")), + List.of(new KeyValueEntry<>("query", "what?")), "http://url/api?query=what%3F" ), Arguments.of( Named.named("percent sign (%) in value", "http://url/api"), - List.of(new QueryParam("discount", "50%")), + List.of(new KeyValueEntry<>("discount", "50%")), "http://url/api?discount=50%25" ), Arguments.of( Named.named("already encoded string - double encoding", "http://url/api"), - List.of(new QueryParam("encoded", "%20")), + List.of(new KeyValueEntry<>("encoded", "%20")), "http://url/api?encoded=%2520" ), Arguments.of( Named.named("URL with port number", "http://localhost:8080/api/v1/data"), - List.of(new QueryParam("key", "value")), + List.of(new KeyValueEntry<>("key", "value")), "http://localhost:8080/api/v1/data?key=value" ), Arguments.of( Named.named("URL with userinfo (auth)", "http://user:password@hostname/path"), - List.of(new QueryParam("secure", "true")), + List.of(new KeyValueEntry<>("secure", "true")), "http://user:password@hostname/path?secure=true" ), Arguments.of( Named.named("IPv4 address in URL", "http://192.168.1.100:9090/endpoint"), - List.of(new QueryParam("ip", "test")), + List.of(new KeyValueEntry<>("ip", "test")), "http://192.168.1.100:9090/endpoint?ip=test" ), Arguments.of( Named.named("IPv6 address in URL", "http://[::1]:8080/api"), - List.of(new QueryParam("ipv6", "true")), + List.of(new KeyValueEntry<>("ipv6", "true")), "http://[::1]:8080/api?ipv6=true" ), Arguments.of( Named.named("HTTPS protocol", "https://secure.example.com/api"), - List.of(new QueryParam("token", "abc123")), + List.of(new KeyValueEntry<>("token", "abc123")), "https://secure.example.com/api?token=abc123" ), Arguments.of( Named.named("pipe (|) in value", "http://url/api"), - List.of(new QueryParam("filter", "a|b|c")), + List.of(new KeyValueEntry<>("filter", "a|b|c")), "http://url/api?filter=a%7Cb%7Cc" ), Arguments.of( Named.named("caret (^) and backtick (`) in value", "http://url/api"), - List.of(new QueryParam("special", "a^b`c")), + List.of(new KeyValueEntry<>("special", "a^b`c")), "http://url/api?special=a%5Eb%60c" ), Arguments.of( Named.named("tab character in value", "http://url/api"), - List.of(new QueryParam("data", "col1\tcol2\tcol3")), + List.of(new KeyValueEntry<>("data", "col1\tcol2\tcol3")), "http://url/api?data=col1%09col2%09col3" ), Arguments.of( Named.named("CRLF in value", "http://url/api"), - List.of(new QueryParam("text", "line1\r\nline2")), + List.of(new KeyValueEntry<>("text", "line1\r\nline2")), "http://url/api?text=line1%0D%0Aline2" ), Arguments.of( Named.named("square brackets in value", "http://url/api"), - List.of(new QueryParam("array", "[1,2,3]")), + List.of(new KeyValueEntry<>("array", "[1,2,3]")), "http://url/api?array=%5B1%2C2%2C3%5D" ), Arguments.of( Named.named("single and double quotes in value", "http://url/api"), - List.of(new QueryParam("quoted", "He said \"Hello\" and 'Hi'")), + List.of(new KeyValueEntry<>("quoted", "He said \"Hello\" and 'Hi'")), "http://url/api?quoted=He%20said%20%22Hello%22%20and%20%27Hi%27" ), Arguments.of( Named.named("angle brackets in value (XSS test)", "http://url/api"), - List.of(new QueryParam("html", "")), + List.of(new KeyValueEntry<>("html", "")), "http://url/api?html=%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E" ), Arguments.of( Named.named("backslash in value", "http://url/api"), - List.of(new QueryParam("path", "C:\\Users\\test")), + List.of(new KeyValueEntry<>("path", "C:\\Users\\test")), "http://url/api?path=C%3A%5CUsers%5Ctest" ), Arguments.of( Named.named("at sign (@) in value", "http://url/api"), - List.of(new QueryParam("contact", "user@domain.com")), + List.of(new KeyValueEntry<>("contact", "user@domain.com")), "http://url/api?contact=user%40domain.com" ), Arguments.of( Named.named("URL as value", "http://url/api"), - List.of(new QueryParam("redirect", "https://example.com/path?foo=bar")), + List.of(new KeyValueEntry<>("redirect", "https://example.com/path?foo=bar")), "http://url/api?redirect=https%3A%2F%2Fexample.com%2Fpath%3Ffoo%3Dbar" ), Arguments.of( @@ -463,89 +463,89 @@ public class TbHttpClientTest { ), Arguments.of( Named.named("underscore and hyphen in key/value", "http://url/api"), - List.of(new QueryParam("my-key_name", "my-value_data")), + List.of(new KeyValueEntry<>("my-key_name", "my-value_data")), "http://url/api?my-key_name=my-value_data" ), Arguments.of( Named.named("dots in key and value", "http://url/api"), - List.of(new QueryParam("version.major", "1.2.3")), + List.of(new KeyValueEntry<>("version.major", "1.2.3")), "http://url/api?version.major=1.2.3" ), Arguments.of( Named.named("multiple reserved chars combined", "http://url/api"), - List.of(new QueryParam("complex", "a=1&b=2#section?query")), + List.of(new KeyValueEntry<>("complex", "a=1&b=2#section?query")), "http://url/api?complex=a%3D1%26b%3D2%23section%3Fquery" ), Arguments.of( Named.named("trailing slash in URL", "http://url/api/"), - List.of(new QueryParam("param", "value")), + List.of(new KeyValueEntry<>("param", "value")), "http://url/api/?param=value" ), Arguments.of( Named.named("long value (1000 chars)", "http://url/api"), - List.of(new QueryParam("long", "a".repeat(1000))), + List.of(new KeyValueEntry<>("long", "a".repeat(1000))), "http://url/api?long=" + "a".repeat(1000) ), Arguments.of( Named.named("Chinese characters in value", "http://url/api"), - List.of(new QueryParam("greeting", "你好世界")), + List.of(new KeyValueEntry<>("greeting", "你好世界")), "http://url/api?greeting=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C" ), Arguments.of( Named.named("Japanese characters in value", "http://url/api"), - List.of(new QueryParam("text", "こんにちは")), + List.of(new KeyValueEntry<>("text", "こんにちは")), "http://url/api?text=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF" ), Arguments.of( Named.named("existing query params in URL", "http://url/api?existing=param"), - List.of(new QueryParam("new", "value")), + List.of(new KeyValueEntry<>("new", "value")), "http://url/api?existing=param&new=value" ), Arguments.of( Named.named("existing query and fragment in URL", "http://url/api?existing=param#section"), - List.of(new QueryParam("additional", "data")), + List.of(new KeyValueEntry<>("additional", "data")), "http://url/api?existing=param&additional=data#section" ), Arguments.of( Named.named("null byte in value", "http://url/api"), - List.of(new QueryParam("data", "before\u0000after")), + List.of(new KeyValueEntry<>("data", "before\u0000after")), "http://url/api?data=before%00after" ), Arguments.of( Named.named("form feed and vertical tab in value", "http://url/api"), - List.of(new QueryParam("whitespace", "a\fb\u000Bc")), + List.of(new KeyValueEntry<>("whitespace", "a\fb\u000Bc")), "http://url/api?whitespace=a%0Cb%0Bc" ), Arguments.of( Named.named("tilde (unreserved, not encoded)", "http://url/api"), - List.of(new QueryParam("pattern", "~user")), + List.of(new KeyValueEntry<>("pattern", "~user")), "http://url/api?pattern=~user" ), Arguments.of( Named.named("asterisk in value", "http://url/api"), - List.of(new QueryParam("wildcard", "*.txt")), + List.of(new KeyValueEntry<>("wildcard", "*.txt")), "http://url/api?wildcard=%2A.txt" ), Arguments.of( Named.named("curly braces in key", "http://url/api"), - List.of(new QueryParam("{key}", "value")), + List.of(new KeyValueEntry<>("{key}", "value")), "http://url/api?%7Bkey%7D=value" ), Arguments.of( Named.named("curly braces in value", "http://url/api"), - List.of(new QueryParam("data", "{value}")), + List.of(new KeyValueEntry<>("data", "{value}")), "http://url/api?data=%7Bvalue%7D" ), Arguments.of( Named.named("curly braces in both key and value", "http://url/api"), - List.of(new QueryParam("{param}", "{data}")), + List.of(new KeyValueEntry<>("{param}", "{data}")), "http://url/api?%7Bparam%7D=%7Bdata%7D" ), Arguments.of( Named.named("duplicate param names with different values", "http://url/api"), List.of( - new QueryParam("test", "a+b"), - new QueryParam("test", "b c") + new KeyValueEntry<>("test", "a+b"), + new KeyValueEntry<>("test", "b c") ), "http://url/api?test=a%2Bb&test=b%20c" ) From 4b0f776c139708193d3b72db3ad20299462dce2f Mon Sep 17 00:00:00 2001 From: Dmytro Zolotarenko Date: Fri, 23 Jan 2026 15:19:19 +0200 Subject: [PATCH 027/313] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6267cc3a7a..77a758ab73 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ ThingsBoard is a scalable, user-friendly, and device-agnostic IoT platform that [**Fleet tracking**](https://thingsboard.io/use-cases/fleet-tracking/) -[![Fleet tracking](https://github.com/user-attachments/assets/9e8938ba-ee0c-4599-9494-d74b7de8a63d "Fleet tracking")](https://thingsboard.io/use-cases/fleet-tracking/) +[![Fleet tracking](https://github.com/user-attachments/assets/d6ce0766-b138-4a42-86aa-7112a543026c "Fleet tracking")](https://thingsboard.io/use-cases/fleet-tracking/) [**Smart farming**](https://thingsboard.io/use-cases/smart-farming/) From 69afda634b25f10cb092104138859eb158f5c0e1 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 23 Jan 2026 15:22:33 +0200 Subject: [PATCH 028/313] lwm2m - execute with params --- .../lwm2m/client/SimpleLwM2MDevice.java | 9 +++- .../sql/RpcLwm2mIntegrationExecuteTest.java | 51 +++++++++++++++++-- .../DefaultLwM2mDownlinkMsgHandler.java | 12 +++-- .../rpc/DefaultLwM2MRpcRequestHandler.java | 6 +++ .../lwm2m/server/rpc/RpcExecuteRequest.java | 27 ++++++++++ 5 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcExecuteRequest.java diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index 191bc97579..fb94fa2a91 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -160,7 +160,14 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl if (!arguments.isEmpty()) withArguments = " with arguments " + arguments; log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); - return ExecuteResponse.success(); + switch (resourceId) { + case 4: + return ExecuteResponse.success(); + case 5: + return ExecuteResponse.success(); + default: + return super.execute(identity, resourceId, arguments); + } } @Override diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java index f5b4538c1c..6de02d6a5e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java @@ -29,6 +29,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_4; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_5; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_8; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; @@ -90,14 +91,54 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT /** - * execute_resource_with_parameters (execute reboot after 60 seconds on device) - * Execute {"id":"3/0/4","value":60} + * execute_resource_with_parameters (execute reboot if digit = 5 on device) + * Execute {"id":"3/0/4","value":5} * {"result":"CHANGED"} */ @Test - public void testExecuteResourceWithParametersById_Result_CHANGED() throws Exception { + public void testExecuteResourceWithParametersOnlyOneDigitValueNullById_Result_Ok() throws Exception { String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_4; - Object expectedValue = 60; + Object expectedValue = 5; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + } + + /** + * execute_resource_with_parameters (execute Factory Reset if digit = 2 -> after 60 seconds on device) + * Execute {"id":"3/0/5","value":"2='60'"} + + */ + @Test + public void testExecuteResourceWithParametersDigit2Value60ById_Result_Ok() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; + Object expectedValue = "2='60'"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + } + + /** + * execute_resource_with_parameters (execute Factory Reset after connect with link on device) + * Execute {"id":"3/0/5","value":"2,0='https://thingsboard.io/docs/reference/lwm2m-api/'"} + */ + @Test + public void testExecuteResourceWithParametersDigit2_0_ValueLinkById_Result_Ok() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; + Object expectedValue = "2,0='https://thingsboard.io/docs/reference/lwm2m-api/'"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + } + + /** + * execute_resource_with_parameters (execute Factory Reset after connect with link on device) + * Execute {"id":"3/0/5","value":"0,1,2,3,4,5,6,7,8,9"} + */ + @Test + public void testExecuteResourceWithParametersDigitManyValueNullById_Result_Ok() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; + Object expectedValue = "0,1,2,3,4,5,6,7,8,9"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -178,7 +219,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT } private String sendRPCExecuteWithValueById(String path, Object value) throws Exception { - String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}"; + String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\", \"value\": \"" + value + "\"}}"; return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index 8f638cfa2a..9576ab7228 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -50,6 +50,8 @@ import org.eclipse.leshan.core.request.SimpleDownlinkRequest; import org.eclipse.leshan.core.request.WriteAttributesRequest; import org.eclipse.leshan.core.request.WriteCompositeRequest; import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.request.argument.Arguments; +import org.eclipse.leshan.core.request.argument.InvalidArgumentException; import org.eclipse.leshan.core.request.exception.ClientSleepingException; import org.eclipse.leshan.core.request.exception.InvalidRequestException; import org.eclipse.leshan.core.request.exception.TimeoutException; @@ -116,6 +118,7 @@ import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; import static org.thingsboard.server.common.transport.util.JsonUtils.isBase64; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.createModelsDefault; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.getVerFromPathIdVerOrId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.validateVersionedId; @@ -281,8 +284,11 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } else if (resourceModelExecute.operations.isExecutable()) { ExecuteRequest downlink; if (request.getParams() != null && !resourceModelExecute.multiple) { - downlink = new ExecuteRequest(request.getObjectId(), (String) this.converter.convertValue(request.getParams(), - resourceModelExecute.type, ResourceModel.Type.STRING, new LwM2mPath(request.getObjectId()))); + Object params = request.getParams(); + ResourceModel.Type resourceModel = resourceModelExecute.type == ResourceModel.Type.NONE ? equalsResourceTypeGetSimpleName(params) : resourceModelExecute.type; + String args = (String) this.converter.convertValue(params, resourceModel, ResourceModel.Type.STRING, new LwM2mPath(request.getObjectId())); + Arguments arguments = Arguments.parse(args); + downlink = new ExecuteRequest(request.getObjectId(), arguments); } else { downlink = new ExecuteRequest(request.getObjectId()); } @@ -290,7 +296,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } else { callback.onValidationError(request.toString(), "Resource with " + request.getVersionedId() + " is not executable."); } - } catch (InvalidRequestException e) { + } catch (InvalidRequestException | InvalidArgumentException e) { callback.onValidationError(request.toString(), e.getMessage()); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java index 6980fb6341..e3bf6e8daa 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java @@ -241,7 +241,13 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { } private void sendExecuteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { + RpcExecuteRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcExecuteRequest.class); TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); + if (!requestMsg.getParams().isEmpty()) { + downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId) + .params(requestBody.getValue()) + .timeout(clientContext.getRequestTimeout(client)).build(); + } var mainCallback = new TbLwM2MExecuteCallback(logService, client, versionedId); var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendExecuteRequest(client, downlink, rpcCallback); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcExecuteRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcExecuteRequest.java new file mode 100644 index 0000000000..e0360c6f71 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcExecuteRequest.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.rpc; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class RpcExecuteRequest extends LwM2MRpcRequestHeader { + + private Object value; + +} From 59056658498f8dfa5f50ae831a3994daa5df6af1 Mon Sep 17 00:00:00 2001 From: Dmytro Zolotarenko Date: Fri, 23 Jan 2026 15:24:44 +0200 Subject: [PATCH 029/313] Update README.md, updated use case name --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77a758ab73..7776b1a964 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,9 @@ ThingsBoard is a scalable, user-friendly, and device-agnostic IoT platform that [![SCADA Swimming pool](https://github.com/user-attachments/assets/68fd9e29-99f1-4c16-8c4c-476f4ccb20c0 "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/) -[**Fleet tracking**](https://thingsboard.io/use-cases/fleet-tracking/) +[**Site fleet tracking**](https://thingsboard.io/use-cases/site-fleet-tracking/) -[![Fleet tracking](https://github.com/user-attachments/assets/d6ce0766-b138-4a42-86aa-7112a543026c "Fleet tracking")](https://thingsboard.io/use-cases/fleet-tracking/) +[![Site fleet tracking](https://github.com/user-attachments/assets/d6ce0766-b138-4a42-86aa-7112a543026c "Site fleet tracking")](https://thingsboard.io/use-cases/site-fleet-tracking/) [**Smart farming**](https://thingsboard.io/use-cases/smart-farming/) From 7900c51b9db07339a29893e89fa49823a056a1ec Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 23 Jan 2026 15:57:21 +0200 Subject: [PATCH 030/313] Version set to 4.4.0-SNAPSHOT --- application/pom.xml | 2 +- .../service/install/DefaultDatabaseSchemaSettingsService.java | 2 +- application/src/main/resources/thingsboard.yml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/package.json | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 4 ++-- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/package.json | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/package.json | 2 +- ui-ngx/pom.xml | 2 +- 65 files changed, 67 insertions(+), 67 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index d2cc11f069..0f3c9623f9 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard application diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 468aa80e6c..b692194382 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -31,7 +31,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // This list should include all versions that are compatible for the upgrade in 4 digits format (like 4.2.0.0, etc.). // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after a new release. // TODO: don't check the "patch" number, since upgrade is not required for patch releases - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.3.0.0"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.3.0.0", "4.3.0.1"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 072e18af69..ef0d83d564 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -217,7 +217,7 @@ ui: # Help parameters help: # Base URL for UI help assets - base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.3}" + base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.4}" # Database telemetry parameters database: diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 64727ab3f7..6326c45685 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 163c47356d..774015fc33 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 9be12f3b18..5983b96b95 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 2e8aca2ba4..32824826c3 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 3468f51be0..12533cd813 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index ccb0eda1f3..0647e706eb 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index 9bcf02dd94..c7658bd738 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index d52074ace6..b5d7b777da 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index b953da6d11..549ee1515f 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 615989da81..478368afdc 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index 211578b1d2..d241646816 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index f024f41992..e24705c25a 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index 4cb3f449a3..1cb159b3af 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index 5f352fb70b..a11d320416 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index d1c71bbd9d..202cc20bd3 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index 0ba928ebe1..b074db321a 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index d2942cd813..da650623f9 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 273e7e2a7d..76ac33b450 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index cd64cc36d2..275a497b6e 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 6f61468ed7..f47255ac7e 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 5521d83c13..985c2a82f6 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index b1c85d22e3..810b22670a 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index eb6ddc4c16..f98b2426dc 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 26c15a8c9f..73e0bdeec0 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index 96e393dc57..f3077d49c3 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index f41fe2b4c0..d9e134128d 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index 0b1874919b..928a107079 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index 57b843bfb9..1081b89276 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 23c857204c..391347e56b 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 6f59dd3a3d..b08d05ed35 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index 2082f134b0..e6adae1f87 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 7d4d858f22..cf7310a084 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "4.3.0.1", + "version": "4.4.0", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 31ea60943d..0e75d24919 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index 188f1f8bb2..27b05f525d 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index c03701ca93..04b1814063 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard msa @@ -133,7 +133,7 @@ - 4.3.0-latest + 4.4.0-latest false diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index d6f08dd6f7..2244241347 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index f9449f97ce..1bf32786e7 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 9809bc2b1e..e89a10e5a1 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 9ec5f75cd0..76b7ff9602 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index a43ef587b9..9c10edfbb7 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 75e8da0d74..ff759a52ff 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 60aa6e123f..92978fcaa7 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index fb28fbba16..049cbad4ec 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index 285761d35a..183a82a170 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 1fbe33abaf..6b0277c2c6 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 13d040b836..4544aece37 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "4.3.0.1", + "version": "4.4.0", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 0a84e52620..7dfc4c0aee 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index c7c077ef38..6856da9233 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard netty-mqtt - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 353c295038..aee23b8090 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index a63cca91fd..9996b19d00 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index a03695f64f..bba58e92a8 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 07719631be..a9f58a08b3 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 97f2ad65b1..4e092211b8 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 4d94ff81df..ed02dbf836 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index d9b28c7a5e..c012e863f9 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index a5d093e726..87f3ccec21 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 5e744d7fca..d92d41388e 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index c53b606704..f99409c470 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index 83c0fccd83..3b6195f9d8 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 39f72755e5..54a949f320 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT transport diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 329f249912..c1385e46d2 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "4.3.0.1", + "version": "4.4.0", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index eaca4ec83c..11ce2dcf5f 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0.1-SNAPSHOT + 4.4.0-SNAPSHOT thingsboard org.thingsboard From 1ea5dbf8256d9efcddc95b0b3c22b8f4dc5c25fe Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 26 Jan 2026 12:46:13 +0200 Subject: [PATCH 031/313] Fix invalid CF config in test --- .../RelatedEntitiesAggregationCalculatedFieldStateTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java index cbde708ff8..02b063a499 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java @@ -234,6 +234,8 @@ public class RelatedEntitiesAggregationCalculatedFieldStateTest { config.setUseLatestTs(true); + config.setScheduledUpdateInterval(10); + calculatedField.setConfiguration(config); calculatedField.setVersion(1L); return calculatedField; From 0403edb9567d864016dbf3b66c5e7b2070add792 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 29 Jan 2026 10:17:02 +0200 Subject: [PATCH 032/313] fixed SwaggerConfiguration to meet open-api standard, fixed overloading apis --- .../server/config/SwaggerConfiguration.java | 2 +- .../server/controller/AdminController.java | 2 +- .../server/controller/AssetController.java | 38 +++-- .../controller/AssetProfileController.java | 7 +- .../server/controller/AuditLogController.java | 15 +- .../server/controller/AuthController.java | 4 +- .../ComponentDescriptorController.java | 4 +- .../server/controller/CustomerController.java | 7 +- .../controller/DashboardController.java | 12 +- .../server/controller/DeviceController.java | 39 +++-- .../controller/DeviceProfileController.java | 7 +- .../server/controller/EdgeController.java | 40 +++-- .../EntitiesVersionControlController.java | 8 +- .../controller/EntityRelationController.java | 154 ++++++++++++------ .../controller/EntityViewController.java | 34 ++-- .../NotificationTargetController.java | 37 ++++- .../server/controller/OAuth2Controller.java | 15 +- .../server/controller/QueueController.java | 8 +- .../controller/QueueStatsController.java | 15 +- .../controller/RuleChainController.java | 6 +- .../controller/TbResourceController.java | 13 +- .../controller/TelemetryController.java | 43 +++-- .../server/controller/TenantController.java | 7 +- .../controller/TenantProfileController.java | 14 +- .../server/controller/UserController.java | 12 +- .../controller/WidgetTypeController.java | 40 ++--- .../controller/WidgetsBundleController.java | 16 +- 27 files changed, 364 insertions(+), 235 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 70259a15ef..3460eb19d9 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -240,7 +240,7 @@ public class SwaggerConfiguration { return GroupedOpenApi.builder() .group(groupName) .pathsToMatch(apiPath) - .addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer)) + //.addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer)) // removed to meet open-api standard .addOperationCustomizer(operationCustomizer()) .addOpenApiCustomizer(customOpenApiCustomizer()) .build(); diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 0b04a6939f..1c76f787ea 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -441,7 +441,7 @@ public class AdminController extends BaseController { .build() + "\""; } - @GetMapping(value = "/mail/oauth2/code", params = {"code", "state"}) + @GetMapping(value = "/mail/oauth2/code") public void codeProcessingUrl( @RequestParam(value = "code") String code, @RequestParam(value = "state") String state, HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 0030896582..ce9b5fbc78 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -23,6 +24,7 @@ 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.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -205,8 +207,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/assets") public PageData getTenantAssets( @Parameter(description = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, @@ -233,8 +234,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets info objects owned by tenant. " + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/assetInfos") public PageData getTenantAssetInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, @@ -262,25 +262,30 @@ public class AssetController extends BaseController { } } - @ApiOperation(value = "Get Tenant Asset (getTenantAsset)", + @Hidden + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/assets", params = {"assetName"}) + public Asset getTenantAsset(@RequestParam String assetName) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName)); + } + + @ApiOperation(value = "Get Tenant Asset (getTenantAssetByName)", notes = "Requested asset must be owned by tenant that the user belongs to. " + "Asset name is an unique property of asset. So it can be used to identify the asset." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET) - @ResponseBody - public Asset getTenantAsset( + @GetMapping(value = "/tenant/asset") + public Asset getTenantAssetByName( @Parameter(description = ASSET_NAME_DESCRIPTION) @RequestParam String assetName) throws ThingsboardException { - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName)); + return getTenantAsset(assetName); } @ApiOperation(value = "Get Customer Assets (getCustomerAssets)", notes = "Returns a page of assets objects assigned to customer. " + PAGE_DATA_PARAMETERS) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/assets") public PageData getCustomerAssets( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -312,8 +317,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets info objects assigned to customer. " + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/assetInfos") public PageData getCustomerAssetInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -349,8 +353,7 @@ public class AssetController extends BaseController { @ApiOperation(value = "Get Assets By Ids (getAssetsByIds)", notes = "Requested assets must be owned by tenant or assigned to customer which user is performing the request. ") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/assets") public List getAssetsByIds( @Parameter(description = "A list of assets ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException, ExecutionException, InterruptedException { @@ -457,8 +460,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets assigned to edge. " + PAGE_DATA_PARAMETERS) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/assets") public PageData getEdgeAssets( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId, 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 0233a488d6..0f0d534a9e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -21,6 +21,7 @@ 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; @@ -170,8 +171,7 @@ public class AssetProfileController extends BaseController { notes = "Returns a page of asset profile objects owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/assetProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/assetProfiles") public PageData getAssetProfiles( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -191,8 +191,7 @@ public class AssetProfileController extends BaseController { notes = "Returns a page of asset profile info objects owned by tenant. " + PAGE_DATA_PARAMETERS + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/assetProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/assetProfileInfos") public PageData getAssetProfileInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index e22c0aa156..da7d2c4583 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -18,11 +18,10 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; 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.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; @@ -73,8 +72,7 @@ public class AuditLogController extends BaseController { "and users actions (login, logout, etc.) that belong to this customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs/customer/{customerId}") public PageData getAuditLogsByCustomerId( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -106,8 +104,7 @@ public class AuditLogController extends BaseController { "For example, RPC call to a particular device, or alarm acknowledgment for a specific device, etc. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/user/{userId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs/user/{userId}") public PageData getAuditLogsByUserId( @Parameter(description = USER_ID_PARAM_DESCRIPTION) @PathVariable("userId") String strUserId, @@ -140,8 +137,7 @@ public class AuditLogController extends BaseController { "For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs/entity/{entityType}/{entityId}") public PageData getAuditLogsByEntityId( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String strEntityType, @@ -175,8 +171,7 @@ public class AuditLogController extends BaseController { notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs") public PageData getAuditLogs( @Parameter(description = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 166fe312f5..530041ce89 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -132,7 +132,7 @@ public class AuthController extends BaseController { "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " + "If token is not valid, returns '409 Conflict'. " + "If token is expired, redirects to error page.") - @GetMapping(value = "/noauth/activate", params = {"activateToken"}) + @GetMapping(value = "/noauth/activate") public ResponseEntity checkActivateToken( @Parameter(description = "The activate token string.") @RequestParam(value = "activateToken") String activateToken) { @@ -172,7 +172,7 @@ public class AuthController extends BaseController { "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " + "If token is not valid, returns '409 Conflict'. " + "If token is expired, redirects to error page.") - @GetMapping(value = "/noauth/resetPassword", params = {"resetToken"}) + @GetMapping(value = "/noauth/resetPassword") public ResponseEntity checkResetToken( @Parameter(description = "The reset token string.") @RequestParam(value = "resetToken") String resetToken) { diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 55dbb8fe04..91569261b7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -81,8 +82,7 @@ public class ComponentDescriptorController extends BaseController { notes = "Gets the Component Descriptors using coma separated list of rule node types and optional rule chain type request parameters. " + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") - @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/components") public List getComponentDescriptorsByTypes( @Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("componentTypes") String[] strComponentTypes, 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 a29d8ab4c8..87948de81f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -22,6 +22,7 @@ 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; @@ -153,8 +154,7 @@ public class CustomerController extends BaseController { notes = "Returns a page of customers owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customers", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customers") public PageData getCustomers( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -174,8 +174,7 @@ public class CustomerController extends BaseController { @ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)", notes = "Get the Customer using Customer Title. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/customers", params = {"customerTitle"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/customers") public Customer getTenantCustomer( @Parameter(description = "A string value representing the Customer title.") @RequestParam String customerTitle) throws ThingsboardException { 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 16f9e8f299..0f8a1a465b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -331,8 +331,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/{tenantId}/dashboards") public PageData getTenantDashboards( @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TENANT_ID) String strTenantId, @@ -356,8 +355,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects owned by the tenant of a current user. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/dashboards") public PageData getTenantDashboards( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -384,8 +382,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects owned by the specified customer. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/dashboards") public PageData getCustomerDashboards( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -583,8 +580,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects assigned to the specified edge. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/dashboards") public PageData getEdgeDashboards( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 03fc5552c9..a89679b24f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -29,6 +30,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; 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.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -331,8 +333,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/devices") public PageData getTenantDevices( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -359,8 +360,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices info objects owned by tenant. " + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/deviceInfos") public PageData getTenantDeviceInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -392,25 +392,31 @@ public class DeviceController extends BaseController { return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink)); } - @ApiOperation(value = "Get Tenant Device (getTenantDevice)", - notes = "Requested device must be owned by tenant that the user belongs to. " + - "Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/devices", params = {"deviceName"}) public Device getTenantDevice( - @Parameter(description = DEVICE_NAME_DESCRIPTION) @RequestParam String deviceName) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); } + @ApiOperation(value = "Get Tenant Device (getTenantDeviceByName)", + notes = "Requested device must be owned by tenant that the user belongs to. " + + "Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/device") + public Device getTenantDeviceByName( + @Parameter(description = DEVICE_NAME_DESCRIPTION) + @RequestParam String deviceName) throws ThingsboardException { + return getTenantDevice(deviceName); + } + @ApiOperation(value = "Get Customer Devices (getCustomerDevices)", notes = "Returns a page of devices objects assigned to customer. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/devices") public PageData getCustomerDevices( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -442,8 +448,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices info objects assigned to customer. " + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/deviceInfos") public PageData getCustomerDeviceInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable("customerId") String strCustomerId, @@ -483,8 +488,7 @@ public class DeviceController extends BaseController { @ApiOperation(value = "Get Devices By Ids (getDevicesByIds)", notes = "Requested devices must be owned by tenant or assigned to customer which user is performing the request. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/devices") public List getDevicesByIds( @Parameter(description = "A list of devices ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException { @@ -711,8 +715,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices assigned to edge. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/devices") public PageData getEdgeDevices( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, 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 7cfb9dd274..b8215860a9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -22,6 +22,7 @@ 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; @@ -224,8 +225,7 @@ public class DeviceProfileController extends BaseController { notes = "Returns a page of devices profile objects owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/deviceProfiles") public PageData getDeviceProfiles( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -245,8 +245,7 @@ public class DeviceProfileController extends BaseController { notes = "Returns a page of devices profile info objects owned by tenant. " + PAGE_DATA_PARAMETERS + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/deviceProfileInfos") public PageData getDeviceProfileInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 784153b650..0c406e0d3d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -53,7 +54,6 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; -import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -71,7 +71,6 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -279,7 +278,7 @@ public class EdgeController extends BaseController { notes = "Returns a page of edges info objects owned by tenant. " + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/tenant/edgeInfos") public PageData getTenantEdgeInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -302,17 +301,24 @@ public class EdgeController extends BaseController { } } - @ApiOperation(value = "Get Tenant Edge (getTenantEdge)", - notes = "Requested edge must be owned by tenant or customer that the user belongs to. " + - "Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/tenant/edges", params = {"edgeName"}) - public Edge getTenantEdge(@Parameter(description = "Unique name of the edge", required = true) - @RequestParam String edgeName) throws ThingsboardException { + public Edge getTenantEdge(@RequestParam String edgeName) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName)); } + @ApiOperation(value = "Get Tenant Edge by name (getTenantEdgeByName)", + notes = "Requested edge must be owned by tenant or customer that the user belongs to. " + + "Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/edge") + public Edge getTenantEdgeByName(@Parameter(description = "Unique name of the edge", required = true) + @RequestParam String edgeName) throws ThingsboardException { + return getTenantEdge(edgeName); + } + @ApiOperation(value = "Set root rule chain for provided edge (setEdgeRootRuleChain)", notes = "Change root rule chain of the edge to the new provided rule chain. \n" + "This operation will send a notification to update root rule chain on remote edge service." + TENANT_AUTHORITY_PARAGRAPH) @@ -336,7 +342,7 @@ public class EdgeController extends BaseController { notes = "Returns a page of edges objects assigned to customer. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/edges") public PageData getCustomerEdges( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -371,7 +377,7 @@ public class EdgeController extends BaseController { notes = "Returns a page of edges info objects assigned to customer. " + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/edgeInfos") public PageData getCustomerEdgeInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -402,12 +408,10 @@ public class EdgeController extends BaseController { return checkNotNull(result); } - @ApiOperation(value = "Get Edges By Ids (getEdgesByIds)", - notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/edges", params = {"edgeIds"}) public List getEdgesByIds( - @Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("edgeIds", strEdgeIds); SecurityUser user = getCurrentUser(); @@ -427,6 +431,16 @@ public class EdgeController extends BaseController { return checkNotNull(edges); } + @ApiOperation(value = "Get Edges By Ids (getEdgeList)", + notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/edges/list") + public List getEdgeList( + @Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException { + return getEdgesByIds(strEdgeIds); + } + @ApiOperation(value = "Find related edges (findByQuery)", notes = "Returns all edges that are related to the specific entity. " + "The entity id, relation type, edge types, depth of the search, and other query parameters defined using complex 'EdgeSearchQuery' object. " + diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java index 10f4cd2f51..b8ec907c5d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java @@ -234,7 +234,7 @@ public class EntitiesVersionControlController extends BaseController { "}" + MARKDOWN_CODE_BLOCK_END + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/version/{entityType}/{externalEntityUuid}", params = {"branch", "pageSize", "page"}) + @GetMapping(value = "/version/{entityType}/{externalEntityUuid}") public DeferredResult> listEntityVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable EntityType entityType, @Parameter(description = "A string value representing external entity id. This is `externalId` property of an entity, or otherwise if not set - simply id of this entity.") @@ -263,7 +263,7 @@ public class EntitiesVersionControlController extends BaseController { "If specified branch does not exist - empty page data will be returned. " + "The response structure is the same as for `listEntityVersions` API method." + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/version/{entityType}", params = {"branch", "pageSize", "page"}) + @GetMapping(value = "/version/{entityType}") public DeferredResult> listEntityTypeVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable EntityType entityType, @Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true) @@ -288,7 +288,7 @@ public class EntitiesVersionControlController extends BaseController { "If specified branch does not exist - empty page data will be returned. " + "The response format is the same as for `listEntityVersions` API method." + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/version", params = {"branch", "pageSize", "page"}) + @GetMapping(value = "/version") public DeferredResult> listVersions(@Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true) @RequestParam String branch, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -355,7 +355,7 @@ public class EntitiesVersionControlController extends BaseController { "Returns an object with current entity data and the one at a specific version. " + "Entity data structure is the same as stored in a repository. " + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/diff/{entityType}/{internalEntityUuid}", params = {"versionId"}) + @GetMapping(value = "/diff/{entityType}/{internalEntityUuid}") public DeferredResult compareEntityDataToVersion(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable EntityType entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index a87db28e92..3faeef3ea8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -15,16 +15,19 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; @@ -76,8 +79,7 @@ public class EntityRelationController extends BaseController { "Relations unique key is a combination of from/to entity id and relation type group and relation type. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relation", method = RequestMethod.POST) - @ResponseStatus(value = HttpStatus.OK) + @PostMapping(value = "/relation") public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true) @RequestBody EntityRelation relation) throws ThingsboardException { doSave(relation); @@ -88,8 +90,7 @@ public class EntityRelationController extends BaseController { "Relations unique key is a combination of from/to entity id and relation type group and relation type. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/v2/relation", method = RequestMethod.POST) - @ResponseStatus(value = HttpStatus.OK) + @PostMapping(value = "/v2/relation") public EntityRelation saveRelationV2(@Parameter(description = "A JSON value representing the relation.", required = true) @RequestBody EntityRelation relation) throws ThingsboardException { return doSave(relation); @@ -109,8 +110,7 @@ public class EntityRelationController extends BaseController { @ApiOperation(value = "Delete Relation (deleteRelation)", notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) - @ResponseStatus(value = HttpStatus.OK) + @DeleteMapping(value = "/relation") public void deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @@ -123,8 +123,7 @@ public class EntityRelationController extends BaseController { @ApiOperation(value = "Delete Relation (deleteRelationV2)", notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/v2/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) - @ResponseStatus(value = HttpStatus.OK) + @DeleteMapping(value = "/v2/relation") public EntityRelation deleteRelationV2(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @@ -154,8 +153,7 @@ public class EntityRelationController extends BaseController { notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"entityId", "entityType"}) - @ResponseStatus(value = HttpStatus.OK) + @DeleteMapping(value = "/relations") public void deleteRelations(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam("entityId") String strId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam("entityType") String strType) throws ThingsboardException { checkParameter("entityId", strId); @@ -168,8 +166,7 @@ public class EntityRelationController extends BaseController { @ApiOperation(value = "Get Relation (getRelation)", notes = "Returns relation object between two specified entities if present. Otherwise throws exception. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relation", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) - @ResponseBody + @GetMapping(value = "/relation") public EntityRelation getRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @@ -189,15 +186,11 @@ public class EntityRelationController extends BaseController { return checkNotNull(relationService.getRelation(getTenantId(), fromId, toId, strRelationType, typeGroup)); } - @ApiOperation(value = "Get List of Relations (findByFrom)", - notes = "Returns list of relation objects for the specified entity by the 'from' direction. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE}) - @ResponseBody - public List findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, - @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE}) + public List findByFrom(@RequestParam(FROM_ID) String strFromId, + @RequestParam(FROM_TYPE) String strFromType, @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -207,12 +200,21 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByFrom(getTenantId(), entityId, typeGroup))); } - @ApiOperation(value = "Get List of Relation Infos (findInfoByFrom)", - notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) + @ApiOperation(value = "Get List of Relations (findByFromV2)", + notes = "Returns list of relation objects for the specified entity by the 'from' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE}) - @ResponseBody + @GetMapping(value = "/relations/from/{fromType}/{fromId}") + public List findByFromV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByFrom(strFromId, strFromType, strRelationTypeGroup); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/info", params = {FROM_ID, FROM_TYPE}) public List findInfoByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @@ -225,16 +227,24 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get())); } - @ApiOperation(value = "Get List of Relations (findByFrom)", - notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION) + @ApiOperation(value = "Get List of Relation Infos (findInfoByFromV2)", + notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE, RELATION_TYPE}) - @ResponseBody - public List findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, - @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, - @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @GetMapping(value = "/relations/info/from/{fromType}/{fromId}") + public List findInfoByFromV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException { + return findInfoByFrom(strFromId, strFromType, strRelationTypeGroup); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE, RELATION_TYPE}) + public List findByFrom(@RequestParam(FROM_ID) String strFromId, + @RequestParam(FROM_TYPE) String strFromType, + @RequestParam(RELATION_TYPE) String strRelationType, @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -245,12 +255,22 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup))); } - @ApiOperation(value = "Get List of Relations (findByTo)", - notes = "Returns list of relation objects for the specified entity by the 'to' direction. " + + @ApiOperation(value = "Get List of Relations (findByFromV2)", + notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {TO_ID, TO_TYPE}) - @ResponseBody + @GetMapping(value = "/relations/from/{fromType}/{fromId}/{relationType}") + public List findByFromV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId, + @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByFrom(strFromId, strFromType, strRelationType, strRelationTypeGroup); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations", params = {TO_ID, TO_TYPE}) public List findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @@ -263,12 +283,21 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByTo(getTenantId(), entityId, typeGroup))); } - @ApiOperation(value = "Get List of Relation Infos (findInfoByTo)", - notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) + @ApiOperation(value = "Get List of Relations (findByToV2)", + notes = "Returns list of relation objects for the specified entity by the 'to' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {TO_ID, TO_TYPE}) - @ResponseBody + @GetMapping(value = "/relations/to/{toType}/{toId}") + public List findByToV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByTo(strToId, strToType, strRelationTypeGroup); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/info", params = {TO_ID, TO_TYPE}) public List findInfoByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @@ -281,12 +310,21 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get())); } - @ApiOperation(value = "Get List of Relations (findByTo)", - notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION) + @ApiOperation(value = "Get List of Relation Infos (findInfoByToV2)", + notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {TO_ID, TO_TYPE, RELATION_TYPE}) - @ResponseBody + @GetMapping(value = "/relations/info/to/{toType}/{toId}") + public List findInfoByToV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException { + return findInfoByTo(strToId, strToType, strRelationTypeGroup); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations", params = {TO_ID, TO_TYPE, RELATION_TYPE}) public List findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @@ -301,13 +339,25 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup))); } + @ApiOperation(value = "Get List of Relations (findByToV2)", + notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/to/{toType}/{toId}/{relationType}") + public List findByToV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId, + @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByTo(strToId, strToType, strRelationType, strRelationTypeGroup); + } + @ApiOperation(value = "Find related entities (findByQuery)", notes = "Returns all entities that are related to the specific entity. " + "The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " + "See 'Model' tab of the Parameters for more info.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/relations") public List findByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); 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 15bda4b87a..cdf030135d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -16,12 +16,14 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; 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; @@ -155,18 +157,25 @@ public class EntityViewController extends BaseController { tbEntityViewService.delete(entityView, getCurrentUser()); } - @ApiOperation(value = "Get Entity View by name (getTenantEntityView)", - notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/entityViews", params = {"entityViewName"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/entityViews", params = {"entityViewName"}) public EntityView getTenantEntityView( - @Parameter(description = "Entity View name") @RequestParam String entityViewName) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName)); } + @ApiOperation(value = "Get Entity View by name (getTenantEntityViewV2)", + notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/entityView") + public EntityView getTenantEntityViewV2( + @Parameter(description = "Entity View name") + @RequestParam String entityViewName) throws ThingsboardException { + return getTenantEntityView(entityViewName); + } + @ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)", notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -213,8 +222,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of Entity View objects assigned to customer. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/entityViews") public PageData getCustomerEntityViews( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -246,8 +254,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/entityViewInfos") public PageData getCustomerEntityViewInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -279,8 +286,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/entityViews") public PageData getTenantEntityViews( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -308,8 +314,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/entityViewInfos") public PageData getTenantEntityViewInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -436,8 +441,7 @@ public class EntityViewController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/entityViews") public PageData getEdgeEntityViews( @PathVariable(EDGE_ID) String strEdgeId, @RequestParam int pageSize, diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java index 72c7f15a3b..4775b78e8b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -153,9 +154,7 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration(), pageLink); } - @ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)", - notes = "Returns the list of notification targets found by provided ids." + - SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @GetMapping(value = "/targets", params = {"ids"}) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") public List getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @@ -166,6 +165,17 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), targetsIds); } + @ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIdsV2)", + notes = "Returns the list of notification targets found by provided ids." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping(value = "/targets/list") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public List getNotificationTargetsByIdsV2(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("ids") UUID[] ids, + @AuthenticationPrincipal SecurityUser user) { + return getNotificationTargetsByIds(ids, user); + } + @ApiOperation(value = "Get notification targets (getNotificationTargets)", notes = "Returns the page of notification targets owned by sysadmin or tenant." + NEW_LINE + PAGE_DATA_PARAMETERS + @@ -188,10 +198,7 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink); } - @ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)", - notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE + - PAGE_DATA_PARAMETERS + - SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @GetMapping(value = "/targets", params = "notificationType") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") public PageData getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize, @@ -206,6 +213,22 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findNotificationTargetsByTenantIdAndSupportedNotificationType(user.getTenantId(), notificationType, pageLink); } + @ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationTypeV2)", + notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE + + PAGE_DATA_PARAMETERS + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping(value = "/targets/notificationType/{notificationType}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getNotificationTargetsBySupportedNotificationTypeV2(@PathVariable NotificationType notificationType, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + return getNotificationTargetsBySupportedNotificationType(pageSize, page, textSearch, sortProperty, sortOrder, notificationType, user); + } + @ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)", notes = "Deletes notification target by its id." + NEW_LINE + "This target cannot be referenced by existing scheduled notification requests or any notification rules." + diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 71b8ac27a9..27aa6e557b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -132,17 +133,25 @@ public class OAuth2Controller extends BaseController { return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink); } - @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)", - notes = "Fetch OAuth2 Client info objects based on the provided ids. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) public List findTenantOAuth2ClientInfosByIds( - @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { List oAuth2ClientIds = getOAuth2ClientIds(clientIds); return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); } + @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIdsV2)", + notes = "Fetch OAuth2 Client info objects based on the provided ids. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/oauth2/client/list") + public List findTenantOAuth2ClientInfosByIdsV2( + @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { + return findTenantOAuth2ClientInfosByIds(clientIds); + } + @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/client/{id}") diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 2d584a6e27..dd2cc19fd6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -19,7 +19,9 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; 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.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -65,8 +67,7 @@ public class QueueController extends BaseController { notes = "Returns a page of queues registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/queues") public PageData getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String serviceType, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -122,8 +123,7 @@ public class QueueController extends BaseController { "Remove 'id', 'tenantId' from the request body example (below) to create new Queue entity. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/queues", params = {"serviceType"}, method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/queues") public Queue saveQueue(@Parameter(description = "A JSON value representing the queue.") @RequestBody Queue queue, @Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java b/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java index 1ccc35b875..5412e5f70d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -85,12 +86,10 @@ public class QueueStatsController extends BaseController { return checkNotNull(queueStatsService.findQueueStatsById(getTenantId(), queueStatsId)); } - @ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)", - notes = "Fetch the Queue stats objects based on the provided ids. ") + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/queueStats", params = {"queueStatsIds"}) public List getQueueStatsByIds( - @Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException { checkArrayParameter("queueStatsIds", strQueueStatsIds); List queueStatsIds = new ArrayList<>(); @@ -99,4 +98,14 @@ public class QueueStatsController extends BaseController { } return queueStatsService.findQueueStatsByIds(getTenantId(), queueStatsIds); } + + @ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIdsV2)", + notes = "Fetch the Queue stats objects based on the provided ids. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/queueStats/list") + public List getQueueStatsByIdsV2( + @Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException { + return getQueueStatsByIds(strQueueStatsIds); + } } 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 12c26dedb3..7a27b7d889 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -278,7 +278,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chains (getRuleChains)", notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/ruleChains", params = {"pageSize", "page"}) + @GetMapping(value = "/ruleChains") public PageData getRuleChains( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -406,7 +406,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/ruleChains/export", params = {"limit"}) + @GetMapping(value = "/ruleChains/export") public RuleChainData exportRuleChains( @Parameter(description = "A limit of rule chains to export.", required = true) @RequestParam("limit") int limit) throws ThingsboardException { @@ -502,7 +502,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)", notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}) + @GetMapping(value = "/edge/{edgeId}/ruleChains") public PageData getEdgeRuleChains( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 14c0a3dcb2..73fe3be137 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -266,11 +267,10 @@ public class TbResourceController extends BaseController { } } - @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)") + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/resource", params = {"resourceIds"}) public List getSystemOrTenantResourcesByIds( - @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException { SecurityUser user = getCurrentUser(); List resourceIds = new ArrayList<>(); @@ -280,6 +280,15 @@ public class TbResourceController extends BaseController { return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds); } + @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIdsV2)") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/resource/list", params = {"resourceIds"}) + public List getSystemOrTenantResourcesByIdsV2( + @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException { + return getSystemOrTenantResourcesByIds(resourceUuids); + } + @ApiOperation(value = "Get All Resource Infos (getAllResources)", notes = "Returns a page of Resource Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index 8ff71494df..03017aeb86 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -24,6 +24,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -36,6 +37,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; 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.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -263,8 +265,7 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_END + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/{entityType}/{entityId}/values/timeseries") public DeferredResult getLatestTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @@ -276,7 +277,32 @@ public class TelemetryController extends BaseController { (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes)); } - @ApiOperation(value = "Get time series data (getTimeseries)", + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"keys", "startTs", "endTs"}) + public DeferredResult getTimeseries( + @PathVariable("entityType") String entityType, + @PathVariable("entityId") String entityIdStr, + @RequestParam(name = "keys") String keys, + @RequestParam(name = "startTs") Long startTs, + @RequestParam(name = "endTs") Long endTs, + @RequestParam(name = "intervalType", required = false) IntervalType intervalType, + @RequestParam(name = "interval", defaultValue = "0") Long interval, + @RequestParam(name = "timeZone", required = false) String timeZone, + @Parameter(description = "An integer value that represents a max number of time series data points to fetch." + + " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100")) + @RequestParam(name = "limit", defaultValue = "100") Integer limit, + @RequestParam(name = "agg", defaultValue = "NONE") String aggStr, + @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { + DeferredResult response = new DeferredResult<>(); + Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs, + intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()), + getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor()); + return response; + } + + @ApiOperation(value = "Get time series data (getTimeseriesHistory)", notes = "Returns a range of time series values for specified entity. " + "Returns not aggregated data by default. " + "Use aggregation function ('agg') and aggregation interval ('interval') to enable aggregation of the results on the database / server side. " + @@ -286,9 +312,8 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_END + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"}) - @ResponseBody - public DeferredResult getTimeseries( + @GetMapping(value = "/{entityType}/{entityId}/values/timeseries/history") + public DeferredResult getTimeseriesHistory( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys, @@ -313,11 +338,7 @@ public class TelemetryController extends BaseController { @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { - DeferredResult response = new DeferredResult<>(); - Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs, - intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()), - getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor()); - return response; + return getTimeseries(entityType, entityIdStr, keys, startTs, endTs, intervalType, interval, timeZone, limit, aggStr, orderBy, useStrictDataTypes); } @ApiOperation(value = "Save device attributes (saveDeviceAttributes)", 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 55bd76fdcb..64cf78e219 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -21,6 +21,7 @@ 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; @@ -128,8 +129,7 @@ public class TenantController extends BaseController { @ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenants", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenants") public PageData getTenants( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -148,8 +148,7 @@ public class TenantController extends BaseController { @ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. " + TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenantInfos") public PageData getTenantInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, 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 7d2f84e827..5644175e1f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -240,8 +241,7 @@ public class TenantProfileController extends BaseController { @ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. " + TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenantProfileInfos") public PageData getTenantProfileInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -257,12 +257,18 @@ public class TenantProfileController extends BaseController { return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink)); } + @Hidden @GetMapping(value = "/tenantProfiles", params = {"ids"}) @PreAuthorize("hasAuthority('SYS_ADMIN')") - public List getTenantProfilesByIds(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string"))) - @RequestParam("ids") UUID[] ids) { + public List getTenantProfilesByIds(@RequestParam("ids") UUID[] ids) { return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids); } + @GetMapping(value = "/tenantProfiles/list") + @PreAuthorize("hasAuthority('SYS_ADMIN')") + public List getTenantProfileList(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("ids") UUID[] ids) { + return getTenantProfilesByIds(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 be0528734a..6de2035940 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -276,8 +276,7 @@ public class UserController extends BaseController { notes = "Returns a page of users owned by tenant or customer. The scope depends on authority of the user that performs the request." + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/users", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/users") public PageData getUsers( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -339,8 +338,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Get Tenant Users (getTenantAdmins)", notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/{tenantId}/users") public PageData getTenantAdmins( @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TENANT_ID) String strTenantId, @@ -363,8 +361,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Get Customer Users (getCustomerUsers)", notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/users") public PageData getCustomerUsers( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -412,8 +409,7 @@ public class UserController extends BaseController { "Search is been executed by email, firstName and lastName fields. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/users/assign/{alarmId}") public PageData getUsersForAssign( @Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true) @PathVariable("alarmId") String strAlarmId, diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index bbcf370158..05adf71a3c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -212,11 +213,9 @@ public class WidgetTypeController extends AutoCommitController { } } - @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypesByBundleAlias) (Deprecated)", - notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}) @Deprecated public List getBundleWidgetTypesByBundleAlias( @Parameter(description = "System or Tenant", required = true) @@ -236,8 +235,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetTypes", params = {"widgetsBundleId"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypes", params = {"widgetsBundleId"}) public List getBundleWidgetTypes( @Parameter(description = "Widget Bundle Id", required = true) @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { @@ -245,11 +243,9 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundleId)); } - @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetailsByBundleAlias) (Deprecated)", - notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}) @Deprecated public List getBundleWidgetTypesDetailsByBundleAlias( @Parameter(description = "System or Tenant", required = true) @@ -266,11 +262,9 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundle.getId())); } - @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetails)", - notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}) public List getBundleWidgetTypesDetails( @Parameter(description = "Widget Bundle Id", required = true) @RequestParam("widgetsBundleId") String strWidgetsBundleId, @@ -291,8 +285,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypeFqns") public List getBundleWidgetTypeFqns( @Parameter(description = "Widget Bundle Id", required = true) @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { @@ -300,11 +293,9 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetFqnsByWidgetsBundleId(getTenantId(), widgetsBundleId)); } - @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)", - notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}) @Deprecated public List getBundleWidgetTypesInfosByBundleAlias( @Parameter(description = "System or Tenant", required = true) @@ -354,11 +345,9 @@ public class WidgetTypeController extends AutoCommitController { widgetTypeDeprecatedFilter, widgetTypes, pageLink)); } - @ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)", - notes = "Get the Widget Type based on the provided parameters. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}) @Deprecated public WidgetType getWidgetTypeByBundleAliasAndTypeAlias( @Parameter(description = "System or Tenant", required = true) @@ -382,8 +371,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get Widget Type (getWidgetType)", notes = "Get the Widget Type by FQN. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER, hidden = true) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetType", params = {"fqn"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetType", params = {"fqn"}) public WidgetType getWidgetType( @Parameter(description = "Widget Type fqn", required = true) @RequestParam String fqn) 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 357c23259a..5f9ca66724 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; 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; @@ -220,11 +222,9 @@ public class WidgetsBundleController extends BaseController { } } - @ApiOperation(value = "Get all Widget Bundles (getWidgetsBundles)", - notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/widgetsBundles", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/widgetsBundles") public List getWidgetsBundles() throws ThingsboardException { if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId())); @@ -234,4 +234,12 @@ public class WidgetsBundleController extends BaseController { } } + @ApiOperation(value = "Get all Widget Bundles (getWidgetsBundlesV2)", + notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundles/all") + public List getWidgetsBundlesV2() throws ThingsboardException { + return getWidgetsBundles(); + } + } From 00c055102204d8654133f1e7b5257cf8b4e2a8b5 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Thu, 29 Jan 2026 10:29:30 +0200 Subject: [PATCH 033/313] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7776b1a964..8d3d1ea4a6 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,14 @@
-💡 [Get started](https://thingsboard.io/docs/getting-started-guides/helloworld/) • 🌐 [Website](https://thingsboard.io/) • 📚 [Documentation](https://thingsboard.io/docs/) • 📔 [Blog](https://thingsboard.io/blog/) • ▶️ [Live demo](https://demo.thingsboard.io/signup) • 🔗 [LinkedIn](https://www.linkedin.com/company/thingsboard/posts/?feedView=all) +💡 [Get started](https://thingsboard.io/docs/getting-started-guides/helloworld/) • 🌐 [Website](https://thingsboard.io/) • 📚 [Documentation](https://thingsboard.io/docs/) • 📔 [Blog](https://thingsboard.io/blog/) • 🔗 [LinkedIn](https://www.linkedin.com/company/thingsboard/posts/?feedView=all)
## 🚀 Installation options -* Install ThingsBoard [On-premise](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise) -* Try [ThingsBoard Cloud](https://thingsboard.io/installations/) -* or [Use our Live demo](https://demo.thingsboard.io/signup) +* Install ThingsBoard [on-premises](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise) +* or use [ThingsBoard Cloud](https://thingsboard.io/installations/) ## 💡 Getting started with ThingsBoard From e205004c19e611e7f27c7c1ef4042025a0b37319 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Thu, 29 Jan 2026 10:30:22 +0200 Subject: [PATCH 034/313] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d3d1ea4a6..169a276db4 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,7 @@ ## 🚀 Installation options -* Install ThingsBoard [on-premises](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise) -* or use [ThingsBoard Cloud](https://thingsboard.io/installations/) +Install ThingsBoard [on-premises](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise) or use [ThingsBoard Cloud](https://thingsboard.io/installations/). ## 💡 Getting started with ThingsBoard From 23001780585881e3d502b864ca9f27596be102a9 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 29 Jan 2026 16:26:05 +0200 Subject: [PATCH 035/313] fixed SwaggerConfiguration, fixed WidgetTypeController --- .../server/config/SwaggerConfiguration.java | 2 +- .../server/controller/WidgetTypeController.java | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 3460eb19d9..d8eb98ab57 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -147,7 +147,7 @@ public class SwaggerConfiguration { .bearerFormat("/api/auth/login|X-Authorization"); var openApi = new OpenAPI() - .components(new Components().addSecuritySchemes("HTTP login form", securityScheme)) + .components(new Components().addSecuritySchemes("http_login_form", securityScheme)) .info(info); addDefaultSchemas(openApi); addLoginOperation(openApi); diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index 05adf71a3c..f6671ffa41 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -232,8 +232,7 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundle.getId())); } - @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", - notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/widgetTypes", params = {"widgetsBundleId"}) public List getBundleWidgetTypes( @@ -243,6 +242,16 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundleId)); } + @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", + notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundles/{widgetsBundleId}/widgetTypes") + public List getBundleWidgetTypesV2( + @Parameter(description = "Widget Bundle Id", required = true) + @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + return getBundleWidgetTypes(strWidgetsBundleId); + } + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}) @@ -262,7 +271,8 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundle.getId())); } - @Hidden + @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetails)", + notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}) public List getBundleWidgetTypesDetails( From c93ce94b9eb65af1bad1e89ff7b6823e0892bbae Mon Sep 17 00:00:00 2001 From: Paolo Cristiani <42511852+pgrisu@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:52:43 +0100 Subject: [PATCH 036/313] Allow hide dashboard toolbar option for public dashboards --- .../home/components/dashboard-page/dashboard-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 3438da8904..84d9061b26 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -653,7 +653,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC private hideToolbarSetting(): boolean { if (isDefined(this.dashboard.configuration?.settings?.hideToolbar)) { - const canApplyHideSetting = !this.forceFullscreen || this.isMobileApp; + const canApplyHideSetting = !this.forceFullscreen || this.isMobileApp || this.isPublicUser(); return this.dashboard.configuration.settings.hideToolbar && canApplyHideSetting; } else { return false; From ccc647b4c53a7815148c763f79045079849a758e Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 2 Feb 2026 10:54:49 +0200 Subject: [PATCH 037/313] Cleanup upgrade script from 4.3 to 4.4 --- .../main/data/upgrade/basic/schema_update.sql | 66 ------------------- .../install/ThingsboardInstallService.java | 1 - 2 files changed, 67 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 66243461e2..a2dfeac358 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,69 +14,3 @@ -- limitations under the License. -- --- UPDATE TENANT PROFILE CONFIGURATION START - -UPDATE tenant_profile -SET profile_data = jsonb_set( - profile_data, - '{configuration}', - jsonb_build_object( - 'minAllowedScheduledUpdateIntervalInSecForCF', 10, - 'maxRelationLevelPerCfArgument', 2, - 'maxRelatedEntitiesToReturnPerCfArgument', 100, - 'minAllowedDeduplicationIntervalInSecForCF', 10, - 'minAllowedAggregationIntervalInSecForCF', 60, - 'intermediateAggregationIntervalInSecForCF', 300, - 'cfReevaluationCheckInterval', 60, - 'alarmsReevaluationInterval', 60 - ) - || - jsonb_strip_nulls(profile_data -> 'configuration') -) -WHERE NOT ( - jsonb_strip_nulls(profile_data -> 'configuration') ?& ARRAY[ - 'minAllowedScheduledUpdateIntervalInSecForCF', - 'maxRelationLevelPerCfArgument', - 'maxRelatedEntitiesToReturnPerCfArgument', - 'minAllowedDeduplicationIntervalInSecForCF', - 'minAllowedAggregationIntervalInSecForCF', - 'intermediateAggregationIntervalInSecForCF', - 'cfReevaluationCheckInterval', - 'alarmsReevaluationInterval' - ] -); - --- UPDATE TENANT PROFILE CONFIGURATION END - --- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE START - -ALTER TABLE calculated_field DROP CONSTRAINT IF EXISTS calculated_field_unq_key; -ALTER TABLE calculated_field ADD CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name); - --- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END - --- CALCULATED FIELD OUTPUT STRATEGY UPDATE START - -UPDATE calculated_field -SET configuration = jsonb_set( - configuration::jsonb, - '{output}', - (configuration::jsonb -> 'output') - || jsonb_build_object( - 'strategy', - jsonb_build_object( - 'type', 'RULE_CHAIN' - ) - ), - false - ) -WHERE (configuration::jsonb -> 'output' -> 'strategy') IS NULL; - --- CALCULATED FIELD OUTPUT STRATEGY UPDATE END - --- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE START - -DROP TABLE IF EXISTS calculated_field_link; -ANALYZE calculated_field; - --- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE END 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 8ea989fc01..66695c396f 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -116,7 +116,6 @@ 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(); From 756797980010c3e93c1b3825d7bf2f7ed85d711d Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 3 Feb 2026 14:16:37 +0200 Subject: [PATCH 038/313] Angular 20 migration --- ui-ngx/package.json | 36 +- ui-ngx/src/app/app.component.ts | 7 +- .../ai-model/ai-model-dialog.component.ts | 7 +- .../check-connectivity-dialog.component.ts | 7 +- .../alarm-rule-details-dialog.component.ts | 9 +- .../alarm-rule-dialog.component.ts | 9 +- .../alarm-rule-filter-config.component.ts | 21 +- .../alarm-rule-table-header.component.ts | 7 +- .../alarm-rules-table.component.ts | 11 +- .../alarm-rules/alarm-rules.component.ts | 7 +- ...f-alarm-rule-condition-dialog.component.ts | 9 +- .../cf-alarm-rule-condition.component.ts | 31 +- .../alarm-rules/cf-alarm-rule.component.ts | 31 +- .../cf-alarm-schedule-dialog.component.ts | 9 +- .../cf-alarm-schedule.component.ts | 25 +- .../create-cf-alarm-rules.component.ts | 31 +- ...mplex-filter-predicate-dialog.component.ts | 9 +- .../alarm-rule-filter-dialog.component.ts | 9 +- .../alarm-rule-filter-list.component.ts | 31 +- ...rm-rule-filter-predicate-list.component.ts | 31 +- ...ilter-predicate-no-data-value.component.ts | 31 +- ...m-rule-filter-predicate-value.component.ts | 31 +- .../alarm-rule-filter-predicate.component.ts | 31 +- .../alarm-rule-filter-text.component.ts | 9 +- .../alarm/alarm-assignee-panel.component.ts | 7 +- .../alarm-assignee-select-panel.component.ts | 7 +- .../alarm/alarm-assignee-select.component.ts | 21 +- .../alarm/alarm-assignee.component.ts | 7 +- .../alarm/alarm-comment-dialog.component.ts | 7 +- .../alarm/alarm-comment.component.ts | 7 +- .../alarm/alarm-details-dialog.component.ts | 7 +- .../alarm/alarm-filter-config.component.ts | 21 +- .../alarm/alarm-table-header.component.ts | 7 +- .../components/alarm/alarm-table.component.ts | 7 +- .../aliases-entity-autocomplete.component.ts | 17 +- .../aliases-entity-select-panel.component.ts | 7 +- .../alias/aliases-entity-select.component.ts | 7 +- .../alias/entity-alias-dialog.component.ts | 9 +- .../alias/entity-aliases-dialog.component.ts | 9 +- .../api-key/add-api-key-dialog.component.ts | 7 +- .../api-key-generated-dialog.component.ts | 7 +- .../api-keys-table-dialog.component.ts | 7 +- .../api-key/api-keys-table.component.ts | 9 +- ...dit-api-key-description-panel.component.ts | 9 +- .../add-attribute-dialog.component.ts | 9 +- ...dd-widget-to-dashboard-dialog.component.ts | 9 +- .../attribute/attribute-table.component.ts | 9 +- .../delete-timeseries-panel.component.ts | 7 +- .../edit-attribute-value-panel.component.ts | 9 +- .../audit-log-details-dialog.component.ts | 7 +- .../audit-log/audit-log-filter.component.ts | 21 +- .../audit-log/audit-log-header.component.ts | 7 +- .../audit-log/audit-log-table.component.ts | 7 +- .../calculated-field.component.ts | 7 +- .../calculated-fields-table.component.ts | 11 +- ...lculated-field-argument-panel.component.ts | 7 +- ...culated-field-arguments-table.component.ts | 31 +- ...y-aggregation-arguments-table.component.ts | 31 +- .../propagate-arguments-table.component.ts | 31 +- ...d-aggregation-arguments-table.component.ts | 31 +- .../calculated-field-dialog.component.ts | 9 +- .../entity-aggregation-component.component.ts | 29 +- ...-geofencing-zone-groups-panel.component.ts | 7 +- ...-geofencing-zone-groups-table.component.ts | 31 +- .../geofencing-configuration.component.ts | 29 +- ...alculated-field-metrics-panel.component.ts | 7 +- ...alculated-field-metrics-table.component.ts | 31 +- .../calculated-field-output.component.ts | 31 +- .../propagation-configuration.component.ts | 29 +- ...ntities-aggregation-component.component.ts | 31 +- .../simple-configuration.component.ts | 29 +- ...lculated-field-test-arguments.component.ts | 31 +- ...ated-field-script-test-dialog.component.ts | 9 +- ...lculated-fields-filter-config.component.ts | 21 +- .../calculated-fields-header.component.ts | 7 +- .../add-widget-dialog.component.ts | 11 +- .../dashboard-image-dialog.component.ts | 7 +- .../dashboard-page.component.ts | 11 +- .../dashboard-settings-dialog.component.ts | 9 +- .../dashboard-state.component.ts | 7 +- .../dashboard-toolbar.component.ts | 9 +- .../dashboard-widget-select.component.ts | 9 +- .../dashboard-page/edit-widget.component.ts | 7 +- .../add-new-breakpoint-dialog.component.ts | 5 +- .../layout/dashboard-layout.component.ts | 7 +- ...nage-dashboard-layouts-dialog.component.ts | 9 +- .../layout/move-widgets-dialog.component.ts | 9 +- .../select-dashboard-breakpoint.component.ts | 7 +- .../dashboard-state-dialog.component.ts | 9 +- .../default-state-controller.component.ts | 7 +- .../entity-state-controller.component.ts | 7 +- ...anage-dashboard-states-dialog.component.ts | 7 +- .../states/states-component.directive.ts | 5 +- .../widget-types-panel.component.ts | 7 +- .../dashboard-view.component.ts | 7 +- .../dashboard/dashboard.component.ts | 9 +- .../select-target-layout-dialog.component.ts | 7 +- .../select-target-state-dialog.component.ts | 9 +- .../components/details-panel.component.ts | 7 +- .../copy-device-credentials.component.ts | 7 +- ...vice-credentials-lwm2m-server.component.ts | 31 +- .../device-credentials-lwm2m.component.ts | 31 +- ...device-credentials-mqtt-basic.component.ts | 30 +- .../device/device-credentials.component.ts | 30 +- .../device/device-info-filter.component.ts | 21 +- .../edge-downlink-table-header.component.ts | 7 +- .../edge/edge-downlink-table.component.ts | 7 +- .../entity/add-entity-dialog.component.ts | 9 +- .../entity-debug-settings-button.component.ts | 33 +- .../entity-debug-settings-panel.component.ts | 17 +- .../entity/entities-table.component.ts | 9 +- .../entity/entity-chips.component.ts | 7 +- .../entity/entity-details-page.component.ts | 9 +- .../entity/entity-details-panel.component.ts | 9 +- .../entity/entity-filter-view.component.ts | 21 +- .../entity/entity-filter.component.ts | 21 +- .../event/event-content-dialog.component.ts | 7 +- .../event/event-filter-panel.component.ts | 7 +- .../event/event-table-header.component.ts | 7 +- .../components/event/event-table.component.ts | 7 +- .../boolean-filter-predicate.component.ts | 31 +- ...mplex-filter-predicate-dialog.component.ts | 9 +- .../complex-filter-predicate.component.ts | 21 +- .../filter/filter-dialog.component.ts | 9 +- .../filter/filter-predicate-list.component.ts | 31 +- .../filter-predicate-value.component.ts | 31 +- .../filter/filter-predicate.component.ts | 31 +- .../filter/filter-text.component.ts | 21 +- .../filter-user-info-dialog.component.ts | 9 +- .../filter/filter-user-info.component.ts | 21 +- .../filter/filters-dialog.component.ts | 9 +- .../filter/filters-edit-panel.component.ts | 7 +- .../filter/filters-edit.component.ts | 7 +- .../filter/key-filter-dialog.component.ts | 9 +- .../filter/key-filter-list.component.ts | 31 +- .../numeric-filter-predicate.component.ts | 31 +- .../string-filter-predicate.component.ts | 31 +- .../filter/user-filter-dialog.component.ts | 9 +- .../github-badge/github-badge.component.ts | 7 +- .../notification-bell.component.ts | 7 +- .../send-notification-button.component.ts | 5 +- .../show-notification-popover.component.ts | 7 +- .../add-device-profile-dialog.component.ts | 9 +- ...larm-duration-predicate-value.component.ts | 21 +- .../alarm/alarm-dynamic-value.component.ts | 15 +- .../alarm-rule-condition-dialog.component.ts | 9 +- .../alarm/alarm-rule-condition.component.ts | 31 +- .../profile/alarm/alarm-rule.component.ts | 31 +- .../alarm/alarm-schedule-dialog.component.ts | 9 +- .../alarm/alarm-schedule-info.component.ts | 17 +- .../profile/alarm/alarm-schedule.component.ts | 25 +- .../alarm/create-alarm-rules.component.ts | 31 +- .../alarm/device-profile-alarm.component.ts | 31 +- .../alarm/device-profile-alarms.component.ts | 31 +- .../edit-alarm-details-dialog.component.ts | 9 +- .../asset-profile-autocomplete.component.ts | 17 +- .../profile/asset-profile-dialog.component.ts | 9 +- .../profile/asset-profile.component.ts | 7 +- .../device-profile-autocomplete.component.ts | 17 +- .../device-profile-dialog.component.ts | 9 +- ...ofile-provision-configuration.component.ts | 31 +- .../profile/device-profile.component.ts | 7 +- ...ofile-transport-configuration.component.ts | 31 +- .../common/power-mode-setting.component.ts | 7 +- .../common/time-unit-select.component.ts | 31 +- ...-device-profile-configuration.component.ts | 17 +- ...ofile-transport-configuration.component.ts | 31 +- .../device-profile-configuration.component.ts | 17 +- ...ofile-transport-configuration.component.ts | 31 +- .../lwm2m-attributes-dialog.component.ts | 9 +- .../lwm2m-attributes-key-list.component.ts | 31 +- .../lwm2m/lwm2m-attributes.component.ts | 17 +- ...trap-add-config-server-dialog.component.ts | 5 +- ...wm2m-bootstrap-config-servers.component.ts | 29 +- .../lwm2m-device-config-server.component.ts | 29 +- ...ofile-transport-configuration.component.ts | 30 +- ...m-object-add-instances-dialog.component.ts | 5 +- ...m2m-object-add-instances-list.component.ts | 28 +- .../lwm2m/lwm2m-object-list.component.ts | 29 +- ...erve-attr-telemetry-instances.component.ts | 31 +- ...erve-attr-telemetry-resources.component.ts | 31 +- .../lwm2m-observe-attr-telemetry.component.ts | 31 +- ...ofile-transport-configuration.component.ts | 31 +- ...-profile-communication-config.component.ts | 30 +- .../snmp-device-profile-mapping.component.ts | 30 +- ...ofile-transport-configuration.component.ts | 30 +- .../queue/tenant-profile-queues.component.ts | 31 +- .../tenant-profile-autocomplete.component.ts | 17 +- .../profile/tenant-profile-data.component.ts | 17 +- .../tenant-profile-dialog.component.ts | 9 +- .../profile/tenant-profile.component.ts | 7 +- ...-tenant-profile-configuration.component.ts | 17 +- .../rate-limits-details-dialog.component.ts | 3 +- .../rate-limits/rate-limits-list.component.ts | 31 +- .../rate-limits/rate-limits-text.component.ts | 7 +- .../rate-limits/rate-limits.component.ts | 31 +- .../tenant-profile-configuration.component.ts | 17 +- .../components/queue/queue-form.component.ts | 31 +- .../relation/relation-dialog.component.ts | 9 +- .../relation/relation-filters.component.ts | 21 +- .../relation/relation-table.component.ts | 9 +- .../resources/resources-dialog.component.ts | 9 +- .../resources/resources-library.component.ts | 5 +- .../home/components/router-tabs.component.ts | 7 +- .../rule-chain-autocomplete.component.ts | 17 +- ...vanced-processing-setting-row.component.ts | 23 +- .../advanced-processing-setting.component.ts | 23 +- .../assign-customer-config.component.ts | 7 +- .../action/attributes-config.component.ts | 7 +- .../action/clear-alarm-config.component.ts | 7 +- .../action/create-alarm-config.component.ts | 7 +- .../create-relation-config.component.ts | 7 +- .../delete-attributes-config.component.ts | 7 +- .../delete-relation-config.component.ts | 7 +- .../action/device-profile-config.component.ts | 7 +- .../action/device-state-config.component.ts | 3 +- .../action/generator-config.component.ts | 7 +- .../action/gps-geo-action-config.component.ts | 7 +- .../rule-node/action/log-config.component.ts | 7 +- .../action/math-function-config.component.ts | 7 +- .../action/msg-count-config.component.ts | 7 +- .../action/msg-delay-config.component.ts | 7 +- .../action/push-to-cloud-config.component.ts | 7 +- .../action/push-to-edge-config.component.ts | 7 +- .../action/rpc-reply-config.component.ts | 7 +- .../action/rpc-request-config.component.ts | 7 +- .../save-to-custom-table-config.component.ts | 7 +- ...nd-rest-api-call-reply-config.component.ts | 7 +- .../action/timeseries-config.component.ts | 7 +- .../unassign-customer-config.component.ts | 7 +- .../common/alarm-status-select.component.ts | 17 +- .../common/arguments-map-config.component.ts | 31 +- .../common/credentials-config.component.ts | 31 +- ...device-relations-query-config.component.ts | 21 +- .../common/example-hint.component.ts | 7 +- .../common/kv-map-config-old.component.ts | 31 +- .../common/kv-map-config.component.ts | 31 +- .../math-function-autocomplete.component.ts | 21 +- .../common/message-types-config.component.ts | 21 +- .../common/msg-metadata-chip.component.ts | 15 +- ...put-message-type-autocomplete.component.ts | 31 +- .../relations-query-config-old.component.ts | 21 +- .../relations-query-config.component.ts | 19 +- .../common/select-attributes.component.ts | 25 +- .../common/sv-map-config.component.ts | 31 +- .../rule-node/empty-config.component.ts | 7 +- .../calculate-delta-config.component.ts | 5 +- .../customer-attributes-config.component.ts | 7 +- .../device-attributes-config.component.ts | 7 +- .../entity-details-config.component.ts | 7 +- ...tch-device-credentials-config.component.ts | 5 +- ...elemetry-from-database-config.component.ts | 7 +- .../originator-attributes-config.component.ts | 7 +- .../originator-fields-config.component.ts | 5 +- .../related-attributes-config.component.ts | 7 +- .../tenant-attributes-config.component.ts | 7 +- .../rule-node/external/ai-config.component.ts | 7 +- .../azure-iot-hub-config.component.ts | 7 +- .../external/kafka-config.component.ts | 7 +- .../external/lambda-config.component.ts | 7 +- .../external/mqtt-config.component.ts | 7 +- .../external/notification-config.component.ts | 7 +- .../external/pubsub-config.component.ts | 7 +- .../external/rabbit-mq-config.component.ts | 7 +- .../rest-api-call-config.component.ts | 7 +- .../external/send-email-config.component.ts | 7 +- .../external/send-sms-config.component.ts | 7 +- .../external/slack-config.component.ts | 7 +- .../external/sns-config.component.ts | 7 +- .../external/sqs-config.component.ts | 7 +- .../filter/check-alarm-status.component.ts | 7 +- .../filter/check-message-config.component.ts | 7 +- .../filter/check-relation-config.component.ts | 7 +- .../filter/gps-geo-filter-config.component.ts | 7 +- .../filter/message-type-config.component.ts | 7 +- .../originator-type-config.component.ts | 7 +- .../filter/script-config.component.ts | 7 +- .../filter/switch-config.component.ts | 7 +- .../flow/rule-chain-input.component.ts | 7 +- .../flow/rule-chain-output.component.ts | 7 +- .../change-originator-config.component.ts | 5 +- .../copy-keys-config.component.ts | 7 +- .../deduplication-config.component.ts | 7 +- .../delete-keys-config.component.ts | 7 +- .../node-json-path-config.component.ts | 7 +- .../rename-keys-config.component.ts | 7 +- .../transformation/script-config.component.ts | 7 +- .../to-email-config.component.ts | 7 +- ...ws-sns-provider-configuration.component.ts | 17 +- ...pp-sms-provider-configuration.component.ts | 17 +- .../sms-provider-configuration.component.ts | 17 +- ...io-sms-provider-configuration.component.ts | 17 +- .../vc/auto-commit-settings.component.ts | 7 +- .../vc/complex-version-create.component.ts | 7 +- .../vc/complex-version-load.component.ts | 7 +- .../entity-types-version-create.component.ts | 31 +- .../vc/entity-types-version-load.component.ts | 31 +- .../vc/entity-version-create.component.ts | 7 +- .../vc/entity-version-diff.component.ts | 9 +- .../vc/entity-version-restore.component.ts | 7 +- .../vc/entity-versions-table.component.ts | 7 +- ...remove-other-entities-confirm.component.ts | 7 +- .../vc/repository-settings.component.ts | 7 +- .../vc/version-control.component.ts | 7 +- .../manage-widget-actions-dialog.component.ts | 9 +- .../action/manage-widget-actions.component.ts | 21 +- .../action/widget-action-dialog.component.ts | 9 +- .../alarm-count-basic-config.component.ts | 7 +- .../alarms-table-basic-config.component.ts | 7 +- .../action-button-basic-config.component.ts | 7 +- .../command-button-basic-config.component.ts | 7 +- .../power-button-basic-config.component.ts | 7 +- ...segmented-button-basic-config.component.ts | 7 +- .../toggle-button-basic-config.component.ts | 7 +- .../aggregated-data-key-row.component.ts | 23 +- .../aggregated-data-keys-panel.component.ts | 23 +- ...gated-value-card-basic-config.component.ts | 7 +- .../label-card-basic-config.component.ts | 7 +- ...label-value-card-basic-config.component.ts | 7 +- ...bile-app-qr-code-basic-config.component.ts | 7 +- .../progress-bar-basic-config.component.ts | 7 +- .../simple-card-basic-config.component.ts | 7 +- ...timeseries-table-basic-config.component.ts | 7 +- ...ead-notification-basic-config.component.ts | 7 +- .../value-card-basic-config.component.ts | 7 +- ...value-chart-card-basic-config.component.ts | 7 +- .../chart/bar-chart-basic-config.component.ts | 7 +- ...hart-with-labels-basic-config.component.ts | 7 +- .../chart/comparison-key-row.component.ts | 23 +- .../chart/comparison-keys-table.component.ts | 23 +- .../chart/doughnut-basic-config.component.ts | 7 +- .../chart/flot-basic-config.component.ts | 7 +- .../chart/pie-chart-basic-config.component.ts | 7 +- ...polar-area-chart-basic-config.component.ts | 7 +- .../radar-chart-basic-config.component.ts | 7 +- .../range-chart-basic-config.component.ts | 7 +- ...ime-series-chart-basic-config.component.ts | 7 +- .../basic/common/data-key-row.component.ts | 23 +- .../basic/common/data-keys-panel.component.ts | 33 +- .../common/widget-actions-panel.component.ts | 21 +- .../entities-table-basic-config.component.ts | 7 +- .../entity-count-basic-config.component.ts | 7 +- .../compass-gauge-basic-config.component.ts | 7 +- ...tal-simple-gauge-basic-config.component.ts | 7 +- .../radial-gauge-basic-config.component.ts | 7 +- ...eter-scale-gauge-basic-config.component.ts | 7 +- .../battery-level-basic-config.component.ts | 7 +- ...iquid-level-card-basic-config.component.ts | 7 +- .../signal-strength-basic-config.component.ts | 7 +- .../status-widget-basic-config.component.ts | 7 +- .../basic/map/map-basic-config.component.ts | 7 +- .../single-switch-basic-config.component.ts | 7 +- .../rpc/slider-basic-config.component.ts | 7 +- .../value-stepper-basic-config.component.ts | 7 +- .../scada-symbol-basic-config.component.ts | 7 +- ...-speed-direction-basic-config.component.ts | 7 +- .../widget/config/datasource.component.ts | 31 +- .../widget/config/datasources.component.ts | 31 +- .../widget/config/target-device.component.ts | 31 +- .../timewindow-config-panel.component.ts | 21 +- .../timewindow-style-panel.component.ts | 11 +- .../config/timewindow-style.component.ts | 21 +- .../custom-dialog-container.component.ts | 5 +- .../embed-dashboard-dialog.component.ts | 7 +- .../alarm/alarms-table-widget.component.ts | 7 +- .../button/action-button-widget.component.ts | 9 +- .../button/command-button-widget.component.ts | 9 +- .../button/toggle-button-widget.component.ts | 9 +- .../two-segment-button-widget.component.ts | 9 +- .../aggregated-value-card-widget.component.ts | 7 +- .../lib/cards/api-usage-widget.component.ts | 9 +- .../lib/cards/label-card-widget.component.ts | 9 +- .../label-value-card-widget.component.ts | 9 +- ...otification-type-filter-panel.component.ts | 7 +- .../cards/progress-bar-widget.component.ts | 9 +- .../unread-notification-widget.component.ts | 9 +- .../lib/cards/value-card-widget.component.ts | 7 +- .../value-chart-card-widget.component.ts | 9 +- .../lib/chart/bar-chart-widget.component.ts | 9 +- .../bar-chart-with-labels-widget.component.ts | 9 +- .../lib/chart/doughnut-widget.component.ts | 9 +- .../lib/chart/latest-chart.component.ts | 9 +- .../lib/chart/pie-chart-widget.component.ts | 9 +- .../lib/chart/polar-area-widget.component.ts | 9 +- .../lib/chart/radar-chart-widget.component.ts | 9 +- .../lib/chart/range-chart-widget.component.ts | 9 +- .../time-series-chart-widget.component.ts | 9 +- .../lib/count/count-widget.component.ts | 7 +- .../date-range-navigator.component.ts | 16 +- .../lib/display-columns-panel.component.ts | 7 +- .../lib/edges-overview-widget.component.ts | 7 +- .../entities-hierarchy-widget.component.ts | 7 +- .../entity/entities-table-widget.component.ts | 7 +- .../widget/lib/flot-widget.component.ts | 7 +- .../add-doc-link-dialog.component.ts | 7 +- .../add-quick-link-dialog.component.ts | 7 +- .../home-page/cluster-info-table.component.ts | 7 +- .../configured-features.component.ts | 7 +- .../lib/home-page/doc-link.component.ts | 23 +- .../home-page/doc-links-widget.component.ts | 7 +- .../home-page/edit-links-dialog.component.ts | 7 +- ...ting-started-completed-dialog.component.ts | 9 +- .../getting-started-widget.component.ts | 7 +- .../lib/home-page/quick-link.component.ts | 23 +- .../home-page/quick-links-widget.component.ts | 7 +- .../recent-dashboards-widget.component.ts | 7 +- .../home-page/usage-info-widget.component.ts | 7 +- .../lib/home-page/version-info.component.ts | 7 +- .../battery-level-widget.component.ts | 9 +- .../liquid-level-widget.component.ts | 9 +- .../signal-strength-widget.component.ts | 9 +- .../lib/indicator/status-widget.component.ts | 9 +- .../widget/lib/json-input-widget.component.ts | 7 +- .../components/widget/lib/legend.component.ts | 7 +- .../dialogs/select-entity-dialog.component.ts | 7 +- .../widget/lib/maps/map-widget.component.ts | 9 +- .../panels/map-timeline-panel.component.ts | 9 +- .../select-map-entity-panel.component.ts | 11 +- .../widget/lib/markdown-widget.component.ts | 5 +- .../lib/mobile-app-qrcode-widget.component.ts | 7 +- .../lib/multiple-input-widget.component.ts | 7 +- .../lib/navigation-card-widget.component.ts | 7 +- .../lib/navigation-cards-widget.component.ts | 7 +- .../lib/photo-camera-input.component.ts | 9 +- .../widget/lib/qrcode-widget.component.ts | 7 +- .../widget/lib/rpc/knob.component.ts | 7 +- .../widget/lib/rpc/led-indicator.component.ts | 7 +- .../rpc/persistent-add-dialog.component.ts | 7 +- .../persistent-details-dialog.component.ts | 7 +- .../rpc/persistent-filter-panel.component.ts | 7 +- .../lib/rpc/persistent-table.component.ts | 7 +- .../lib/rpc/power-button-widget.component.ts | 9 +- .../widget/lib/rpc/round-switch.component.ts | 7 +- .../lib/rpc/single-switch-widget.component.ts | 9 +- .../widget/lib/rpc/slider-widget.component.ts | 9 +- .../widget/lib/rpc/switch.component.ts | 7 +- .../lib/rpc/value-stepper-widget.component.ts | 9 +- .../scada/scada-symbol-widget.component.ts | 9 +- .../alarm-count-widget-settings.component.ts | 7 +- .../alarms-table-key-settings.component.ts | 7 +- .../alarms-table-widget-settings.component.ts | 7 +- ...action-button-widget-settings.component.ts | 7 +- ...ommand-button-widget-settings.component.ts | 7 +- .../power-button-widget-settings.component.ts | 7 +- ...mented-button-widget-settings.component.ts | 7 +- ...toggle-button-widget-settings.component.ts | 7 +- ...gated-value-card-key-settings.component.ts | 7 +- ...ed-value-card-widget-settings.component.ts | 7 +- .../cards/api-usage-data-key-row.component.ts | 23 +- .../api-usage-widget-settings.component.ts | 21 +- ...shboard-state-widget-settings.component.ts | 7 +- ...uick-overview-widget-settings.component.ts | 7 +- .../html-card-widget-settings.component.ts | 7 +- .../label-card-widget-settings.component.ts | 7 +- ...el-value-card-widget-settings.component.ts | 7 +- .../cards/label-widget-label.component.ts | 21 +- .../cards/label-widget-settings.component.ts | 7 +- .../markdown-widget-settings.component.ts | 7 +- ...e-app-qr-code-widget-settings.component.ts | 7 +- .../progress-bar-widget-settings.component.ts | 7 +- .../cards/qrcode-widget-settings.component.ts | 7 +- .../simple-card-widget-settings.component.ts | 7 +- ...timeseries-table-key-settings.component.ts | 7 +- ...ies-table-latest-key-settings.component.ts | 7 +- ...eseries-table-widget-settings.component.ts | 7 +- ...-notification-widget-settings.component.ts | 7 +- .../value-card-widget-settings.component.ts | 7 +- ...ue-chart-card-widget-settings.component.ts | 7 +- .../bar-chart-widget-settings.component.ts | 7 +- ...t-with-labels-widget-settings.component.ts | 7 +- .../chart/chart-widget-settings.component.ts | 7 +- ...oughnut-chart-widget-settings.component.ts | 7 +- .../doughnut-widget-settings.component.ts | 7 +- .../chart/flot-bar-key-settings.component.ts | 7 +- .../flot-bar-widget-settings.component.ts | 7 +- .../chart/flot-key-settings.component.ts | 31 +- .../flot-latest-key-settings.component.ts | 7 +- .../chart/flot-line-key-settings.component.ts | 7 +- .../flot-line-widget-settings.component.ts | 7 +- .../chart/flot-pie-key-settings.component.ts | 7 +- .../flot-pie-widget-settings.component.ts | 7 +- .../chart/flot-threshold.component.ts | 23 +- .../chart/flot-widget-settings.component.ts | 31 +- .../chart/label-data-key.component.ts | 21 +- .../pie-chart-widget-settings.component.ts | 7 +- ...ar-area-chart-widget-settings.component.ts | 7 +- .../radar-chart-widget-settings.component.ts | 7 +- .../range-chart-widget-settings.component.ts | 7 +- ...ime-series-chart-key-settings.component.ts | 7 +- ...me-series-chart-line-settings.component.ts | 21 +- ...-series-chart-widget-settings.component.ts | 7 +- .../custom-action-pretty-editor.component.ts | 23 +- ...-action-pretty-resources-tabs.component.ts | 9 +- ...t-value-action-settings-panel.component.ts | 11 +- .../get-value-action-settings.component.ts | 23 +- .../action/map-item-tooltips.component.ts | 19 +- .../action/mobile-action-editor.component.ts | 17 +- ...t-value-action-settings-panel.component.ts | 11 +- .../set-value-action-settings.component.ts | 23 +- .../widget-action-settings-panel.component.ts | 11 +- .../widget-action-settings.component.ts | 23 +- .../common/action/widget-action.component.ts | 31 +- .../common/advanced-range.component.ts | 21 +- .../alias/entity-alias-select.component.ts | 17 +- ...to-date-format-settings-panel.component.ts | 11 +- .../auto-date-format-settings.component.ts | 21 +- .../common/axis-scale-row.component.ts | 29 +- .../background-settings-panel.component.ts | 11 +- .../common/background-settings.component.ts | 23 +- .../widget-button-appearance.component.ts | 23 +- ...get-button-custom-style-panel.component.ts | 11 +- .../widget-button-custom-style.component.ts | 23 +- ...ton-toggle-custom-style-panel.component.ts | 11 +- ...et-button-toggle-custom-style.component.ts | 23 +- .../chart-animation-settings.component.ts | 21 +- .../chart/chart-bar-settings.component.ts | 21 +- .../chart/chart-fill-settings.component.ts | 21 +- ...es-chart-axis-settings-button.component.ts | 21 +- ...ies-chart-axis-settings-panel.component.ts | 11 +- ...me-series-chart-axis-settings.component.ts | 31 +- ...me-series-chart-grid-settings.component.ts | 21 +- .../time-series-chart-state-row.component.ts | 23 +- ...ime-series-chart-states-panel.component.ts | 33 +- ...me-series-chart-threshold-row.component.ts | 23 +- ...hart-threshold-settings-panel.component.ts | 11 +- ...ries-chart-threshold-settings.component.ts | 21 +- ...series-chart-thresholds-panel.component.ts | 33 +- ...ime-series-chart-y-axes-panel.component.ts | 33 +- .../time-series-chart-y-axis-row.component.ts | 23 +- ...ggregation-bar-width-settings.component.ts | 21 +- .../common/color-range-list.component.ts | 23 +- .../common/color-range-panel.component.ts | 11 +- .../common/color-range-settings.component.ts | 21 +- .../common/color-settings-panel.component.ts | 11 +- .../common/color-settings.component.ts | 21 +- .../common/count-widget-settings.component.ts | 21 +- .../common/css-size-input.component.ts | 31 +- .../common/css-unit-select.component.ts | 21 +- .../common/date-format-select.component.ts | 21 +- .../date-format-settings-panel.component.ts | 11 +- .../dynamic-form-array.component.ts | 33 +- .../dynamic-form-properties.component.ts | 33 +- .../dynamic-form-property-panel.component.ts | 9 +- .../dynamic-form-property-row.component.ts | 33 +- .../dynamic-form-select-item-row.component.ts | 33 +- .../dynamic-form-select-items.component.ts | 33 +- .../dynamic-form/dynamic-form.component.ts | 31 +- .../common/entity-alias-input.component.ts | 23 +- .../common/filter/filter-select.component.ts | 25 +- .../common/font-settings-panel.component.ts | 11 +- .../common/font-settings.component.ts | 21 +- .../lib/settings/common/gradient.component.ts | 21 +- .../common/image-cards-select.component.ts | 26 +- .../status-widget-state-settings.component.ts | 21 +- .../key/data-key-config-dialog.component.ts | 9 +- .../common/key/data-key-config.component.ts | 31 +- .../common/key/data-key-input.component.ts | 23 +- .../common/key/data-keys.component.ts | 41 +- .../common/legend-config.component.ts | 21 +- ...dditional-map-data-source-row.component.ts | 23 +- .../additional-map-data-sources.component.ts | 33 +- ...ta-layer-color-settings-panel.component.ts | 11 +- .../data-layer-color-settings.component.ts | 21 +- .../data-layer-pattern-settings.component.ts | 31 +- .../image-map-source-settings.component.ts | 31 +- .../map/map-action-button-row.component.ts | 25 +- .../map-action-buttons-settings.component.ts | 25 +- .../map/map-data-layer-dialog.component.ts | 9 +- .../map/map-data-layer-row.component.ts | 23 +- .../common/map/map-data-layers.component.ts | 33 +- .../map/map-data-source-row.component.ts | 23 +- .../common/map/map-data-sources.component.ts | 33 +- .../common/map/map-layer-row.component.ts | 23 +- .../map/map-layer-settings-panel.component.ts | 11 +- .../common/map/map-layers.component.ts | 33 +- .../common/map/map-settings.component.ts | 31 +- .../map/map-tooltip-tag-actions.component.ts | 23 +- .../marker-clustering-settings.component.ts | 31 +- .../map/marker-icon-shapes.component.ts | 11 +- .../marker-image-settings-panel.component.ts | 11 +- .../map/marker-image-settings.component.ts | 21 +- .../map/marker-shape-settings.component.ts | 21 +- .../common/map/marker-shapes.component.ts | 11 +- ...ape-fill-image-settings-panel.component.ts | 11 +- .../shape-fill-image-settings.component.ts | 21 +- ...pe-fill-stripe-settings-panel.component.ts | 11 +- .../shape-fill-stripe-settings.component.ts | 21 +- .../map/trip-timeline-settings.component.ts | 31 +- .../scada-symbol-object-settings.component.ts | 31 +- .../common/value-source-data-key.component.ts | 21 +- .../settings/common/value-source.component.ts | 21 +- .../settings/common/widget-font.component.ts | 21 +- .../widget/widget-settings.component.ts | 27 +- .../device-key-autocomplete.component.ts | 21 +- .../knob-control-widget-settings.component.ts | 7 +- ...led-indicator-widget-settings.component.ts | 7 +- ...sistent-table-widget-settings.component.ts | 7 +- .../round-switch-widget-settings.component.ts | 7 +- .../control/rpc-button-style.component.ts | 21 +- .../rpc-shell-widget-settings.component.ts | 7 +- .../rpc-terminal-widget-settings.component.ts | 7 +- .../send-rpc-widget-settings.component.ts | 7 +- ...single-switch-widget-settings.component.ts | 7 +- .../slide-toggle-widget-settings.component.ts | 7 +- .../slider-widget-settings.component.ts | 7 +- ...witch-control-widget-settings.component.ts | 7 +- .../control/switch-rpc-settings.component.ts | 31 +- ...ice-attribute-widget-settings.component.ts | 7 +- ...value-stepper-widget-settings.component.ts | 7 +- ...nge-navigator-widget-settings.component.ts | 7 +- ...ies-hierarchy-widget-settings.component.ts | 7 +- .../entities-table-key-settings.component.ts | 7 +- ...ntities-table-widget-settings.component.ts | 7 +- .../entity-count-widget-settings.component.ts | 7 +- ...single-device-widget-settings.component.ts | 7 +- ...ateway-config-widget-settings.component.ts | 7 +- ...ateway-events-widget-settings.component.ts | 7 +- .../gateway-logs-settings.component.ts | 7 +- .../gateway-service-rpc-settings.component.ts | 7 +- ...logue-compass-widget-settings.component.ts | 7 +- ...-linear-gauge-widget-settings.component.ts | 7 +- ...-radial-gauge-widget-settings.component.ts | 7 +- ...digital-gauge-widget-settings.component.ts | 7 +- .../settings/gauge/tick-value.component.ts | 21 +- .../gpio-control-widget-settings.component.ts | 7 +- .../lib/settings/gpio/gpio-item.component.ts | 21 +- .../gpio-panel-widget-settings.component.ts | 7 +- .../doc-links-widget-settings.component.ts | 7 +- .../quick-links-widget-settings.component.ts | 7 +- ...battery-level-widget-settings.component.ts | 7 +- ...id-level-card-widget-settings.component.ts | 7 +- ...gnal-strength-widget-settings.component.ts | 7 +- .../status-widget-settings.component.ts | 7 +- .../input/datakey-select-option.component.ts | 21 +- ...vice-claiming-widget-settings.component.ts | 7 +- ...-camera-input-widget-settings.component.ts | 7 +- ...te-attribute-general-settings.component.ts | 31 +- ...ean-attribute-widget-settings.component.ts | 7 +- ...ate-attribute-widget-settings.component.ts | 7 +- ...ble-attribute-widget-settings.component.ts | 7 +- ...age-attribute-widget-settings.component.ts | 7 +- ...ger-attribute-widget-settings.component.ts | 7 +- ...son-attribute-widget-settings.component.ts | 7 +- ...ion-attribute-widget-settings.component.ts | 7 +- ...tiple-attributes-key-settings.component.ts | 7 +- ...le-attributes-widget-settings.component.ts | 7 +- ...ing-attribute-widget-settings.component.ts | 7 +- .../map/legacy/circle-settings.component.ts | 31 +- .../legacy/common-map-settings.component.ts | 31 +- .../datasources-key-autocomplete.component.ts | 21 +- .../google-map-provider-settings.component.ts | 31 +- .../here-map-provider-settings.component.ts | 31 +- .../image-map-provider-settings.component.ts | 31 +- .../legacy/map-editor-settings.component.ts | 31 +- .../legacy/map-provider-settings.component.ts | 31 +- .../legacy/map-settings-legacy.component.ts | 31 +- .../map-widget-settings-legacy.component.ts | 7 +- .../marker-clustering-settings.component.ts | 31 +- .../map/legacy/markers-settings.component.ts | 31 +- ...nstreet-map-provider-settings.component.ts | 31 +- .../map/legacy/polygon-settings.component.ts | 31 +- .../legacy/route-map-settings.component.ts | 31 +- .../route-map-widget-settings.component.ts | 7 +- ...tencent-map-provider-settings.component.ts | 31 +- ...rip-animation-common-settings.component.ts | 31 +- ...rip-animation-marker-settings.component.ts | 31 +- .../trip-animation-path-settings.component.ts | 31 +- ...trip-animation-point-settings.component.ts | 31 +- ...rip-animation-widget-settings.component.ts | 7 +- .../map/map-widget-settings.component.ts | 7 +- ...vigation-card-widget-settings.component.ts | 7 +- ...igation-cards-widget-settings.component.ts | 7 +- .../scada-symbol-widget-settings.component.ts | 7 +- ...eed-direction-widget-settings.component.ts | 7 +- .../lib/timeseries-table-widget.component.ts | 7 +- .../trip-animation.component.ts | 9 +- .../wind-speed-direction-widget.component.ts | 9 +- .../widget/widget-config.component.ts | 31 +- .../widget/widget-container.component.ts | 18 +- .../widget/widget-preview.component.ts | 7 +- .../components/widget/widget.component.ts | 11 +- .../wizard/device-wizard-dialog.component.ts | 7 +- ...d-entities-to-customer-dialog.component.ts | 9 +- .../add-entities-to-edge-dialog.component.ts | 9 +- .../assign-to-customer-dialog.component.ts | 9 +- .../home/dialogs/events-dialog.component.ts | 7 +- ui-ngx/src/app/modules/home/home.component.ts | 7 +- .../modules/home/menu/menu-link.component.ts | 9 +- .../home/menu/menu-toggle.component.ts | 9 +- .../modules/home/menu/side-menu.component.ts | 9 +- .../auto-commit-admin-settings.component.ts | 7 +- .../pages/admin/general-settings.component.ts | 7 +- .../pages/admin/home-settings.component.ts | 7 +- .../home/pages/admin/mail-server.component.ts | 7 +- .../oauth2/clients/client-dialog.component.ts | 9 +- .../clients/client-table-header.component.ts | 7 +- .../admin/oauth2/clients/client.component.ts | 7 +- .../domains/domain-table-header.component.ts | 7 +- .../admin/oauth2/domains/domain.component.ts | 7 +- .../home/pages/admin/queue/queue.component.ts | 7 +- .../repository-admin-settings.component.ts | 7 +- .../js-library-table-header.component.ts | 7 +- .../admin/resource/js-resource.component.ts | 5 +- .../resource-library-tabs.component.ts | 7 +- .../admin/resource/resource-tabs.component.ts | 7 +- .../resources-table-header.component.ts | 7 +- .../admin/security-settings.component.ts | 7 +- .../admin/send-test-sms-dialog.component.ts | 7 +- .../pages/admin/sms-provider.component.ts | 7 +- .../pages/admin/trendz-settings.component.ts | 7 +- .../two-factor-auth-settings.component.ts | 7 +- .../ai-model-table-header.component.ts | 9 +- .../pages/alarm/alarm-rules-tabs.component.ts | 7 +- .../asset-profile-tabs.component.ts | 7 +- .../asset/asset-table-header.component.ts | 7 +- .../home/pages/asset/asset-tabs.component.ts | 7 +- .../home/pages/asset/asset.component.ts | 7 +- .../calculated-fields-tabs.component.ts | 7 +- .../pages/customer/customer-tabs.component.ts | 7 +- .../home/pages/customer/customer.component.ts | 7 +- .../dashboard/dashboard-form.component.ts | 7 +- .../dashboard/dashboard-tabs.component.ts | 7 +- .../import-dashboard-file-dialog.component.ts | 7 +- .../make-dashboard-public-dialog.component.ts | 7 +- ...ge-dashboard-customers-dialog.component.ts | 9 +- .../device-profile-tabs.component.ts | 7 +- ...evice-transport-configuration.component.ts | 17 +- .../default-device-configuration.component.ts | 17 +- ...evice-transport-configuration.component.ts | 17 +- .../data/device-configuration.component.ts | 17 +- .../device/data/device-data.component.ts | 31 +- ...evice-transport-configuration.component.ts | 30 +- ...evice-transport-configuration.component.ts | 17 +- ...evice-transport-configuration.component.ts | 17 +- ...evice-transport-configuration.component.ts | 28 +- ...ice-check-connectivity-dialog.component.ts | 7 +- .../device-credentials-dialog.component.ts | 9 +- .../device/device-table-header.component.ts | 7 +- .../pages/device/device-tabs.component.ts | 7 +- .../home/pages/device/device.component.ts | 7 +- .../edge-instructions-dialog.component.ts | 7 +- .../pages/edge/edge-table-header.component.ts | 7 +- .../home/pages/edge/edge-tabs.component.ts | 7 +- .../modules/home/pages/edge/edge.component.ts | 7 +- .../entity-view-table-header.component.ts | 7 +- .../entity-view/entity-view-tabs.component.ts | 7 +- .../entity-view/entity-view.component.ts | 7 +- .../pages/home-links/home-links.component.ts | 9 +- .../mobile-app-dialog.component.ts | 9 +- .../mobile-app-table-header.component.ts | 7 +- .../applications/mobile-app.component.ts | 7 +- .../remove-app-dialog.component.ts | 7 +- .../add-mobile-page-dialog.component.ts | 7 +- .../custom-mobile-page-panel.component.ts | 9 +- .../layout/custom-mobile-page.component.ts | 31 +- .../default-mobile-page-panel.component.ts | 9 +- .../bundes/layout/mobile-layout.component.ts | 31 +- .../layout/mobile-page-item-row.component.ts | 33 +- ...bile-app-configuration-dialog.component.ts | 7 +- .../bundes/mobile-bundle-dialog.component.ts | 7 +- .../mobile-bundle-table-header.component.ts | 7 +- .../mobile/common/editor-panel.component.ts | 9 +- ...obile-qr-code-widget-settings.component.ts | 7 +- .../inbox-notification-dialog.component.ts | 7 +- .../inbox/inbox-table-header.component.ts | 7 +- ...recipient-notification-dialog.component.ts | 7 +- .../rule/escalation-form.component.ts | 31 +- .../rule/escalations.component.ts | 31 +- .../rule-notification-dialog.component.ts | 7 +- .../sent/sent-error-dialog.component.ts | 7 +- .../sent/sent-notification-dialog.componet.ts | 7 +- .../notification-setting-form.component.ts | 21 +- .../notification-settings.component.ts | 7 +- ...n-action-button-configuration.component.ts | 29 +- ...cation-template-configuration.component.ts | 31 +- .../template-notification-dialog.component.ts | 7 +- .../ota-update/ota-update-tabs.component.ts | 7 +- .../pages/ota-update/ota-update.component.ts | 5 +- .../home/pages/profile/profile.component.ts | 7 +- .../pages/rulechain/link-labels.component.ts | 17 +- .../rulechain/rule-node-config.component.ts | 19 +- .../rulechain/rule-node-details.component.ts | 7 +- .../rulechain/rule-node-link.component.ts | 17 +- .../rulechain/rulechain-page.component.ts | 36 +- .../rulechain/rulechain-tabs.component.ts | 7 +- .../pages/rulechain/rulechain.component.ts | 7 +- .../pages/rulechain/rulenode.component.ts | 9 +- .../scada-symbol-behavior-panel.component.ts | 9 +- .../scada-symbol-behavior-row.component.ts | 33 +- .../scada-symbol-behaviors.component.ts | 33 +- ...l-metadata-tag-function-panel.component.ts | 9 +- .../scada-symbol-metadata-tag.component.ts | 33 +- .../scada-symbol-metadata-tags.component.ts | 33 +- .../scada-symbol-metadata.component.ts | 33 +- .../scada-symbol-editor.component.ts | 9 +- .../scada-symbol-tooltip.components.ts | 35 +- .../scada-symbol/scada-symbol.component.ts | 9 +- .../backup-code-auth-dialog.component.ts | 7 +- .../email-auth-dialog.component.ts | 7 +- .../sms-auth-dialog.component.ts | 7 +- .../totp-auth-dialog.component.ts | 7 +- .../home/pages/security/security.component.ts | 7 +- .../tenant-profile-tabs.component.ts | 7 +- .../pages/tenant/tenant-tabs.component.ts | 7 +- .../home/pages/tenant/tenant.component.ts | 7 +- .../user/activation-link-dialog.component.ts | 5 +- .../pages/user/add-user-dialog.component.ts | 7 +- .../home/pages/user/user-tabs.component.ts | 7 +- .../modules/home/pages/user/user.component.ts | 7 +- .../save-widget-type-as-dialog.component.ts | 7 +- .../select-widget-type-dialog.component.ts | 7 +- .../pages/widget/widget-editor.component.ts | 9 +- .../widget-type-autocomplete.component.ts | 19 +- .../widget/widget-type-tabs.component.ts | 7 +- .../pages/widget/widget-type.component.ts | 7 +- .../widget/widgets-bundle-dialog.component.ts | 9 +- .../widget/widgets-bundle-tabs.component.ts | 7 +- .../widgets-bundle-widgets.component.ts | 7 +- .../pages/widget/widgets-bundle.component.ts | 7 +- .../pages/login/create-password.component.ts | 7 +- .../force-two-factor-auth-login.component.ts | 7 +- .../pages/login/link-expired.component.ts | 7 +- .../login/pages/login/login.component.ts | 7 +- .../login/reset-password-request.component.ts | 7 +- .../pages/login/reset-password.component.ts | 7 +- .../login/two-factor-auth-login.component.ts | 7 +- .../shared/components/breadcrumb.component.ts | 9 +- .../button/copy-button.component.ts | 7 +- .../button/toggle-password.component.ts | 7 +- .../button/widget-button-toggle.component.ts | 7 +- .../button/widget-button.component.ts | 9 +- .../shared/components/cheatsheet.component.ts | 7 +- .../components/circular-progress.directive.ts | 5 +- .../components/color-input.component.ts | 21 +- .../color-picker-panel.component.ts | 11 +- .../color-picker/color-picker.component.ts | 21 +- .../color-picker/hex-input.component.ts | 9 +- .../shared/components/contact.component.ts | 5 +- .../country-autocomplete.component.ts | 33 +- .../app/shared/components/css.component.ts | 33 +- .../dashboard-autocomplete.component.ts | 17 +- .../dashboard-select-panel.component.ts | 7 +- .../components/dashboard-select.component.ts | 17 +- .../dashboard-state-autocomplete.component.ts | 17 +- .../dialog/alert-dialog.component.ts | 7 +- .../dialog/color-picker-dialog.component.ts | 7 +- .../dialog/confirm-dialog.component.ts | 7 +- .../entity-conflict-dialog.component.ts | 15 +- .../entity-limit-exceeded-dialog.component.ts | 9 +- .../dialog/error-alert-dialog.component.ts | 7 +- .../json-object-edit-dialog.component.ts | 7 +- .../dialog/material-icons-dialog.component.ts | 9 +- .../node-script-test-dialog.component.ts | 11 +- .../dialog/todo-dialog.component.ts | 7 +- .../directives/component-outlet.directive.ts | 7 +- .../sring-template-outlet.directive.ts | 7 +- .../directives/tb-json-to-string.directive.ts | 39 +- .../entity/entity-autocomplete.component.ts | 17 +- .../entity/entity-gateway-select.component.ts | 17 +- .../entity-key-autocomplete.component.ts | 29 +- .../entity/entity-keys-list.component.ts | 21 +- .../entity/entity-list-select.component.ts | 17 +- .../entity/entity-list.component.ts | 31 +- .../entity/entity-select.component.ts | 17 +- .../entity-subtype-autocomplete.component.ts | 17 +- .../entity/entity-subtype-list.component.ts | 21 +- .../entity/entity-subtype-select.component.ts | 17 +- .../entity/entity-type-list.component.ts | 21 +- .../entity/entity-type-select.component.ts | 17 +- .../components/fab-toolbar.component.ts | 21 +- .../shared/components/file-input.component.ts | 21 +- .../footer-fab-buttons.component.ts | 9 +- .../app/shared/components/footer.component.ts | 7 +- .../shared/components/fullscreen.directive.ts | 5 +- .../components/grid/scroll-grid.component.ts | 9 +- .../components/help-markdown.component.ts | 7 +- .../shared/components/help-popup.component.ts | 9 +- .../app/shared/components/help.component.ts | 5 +- .../components/hint-tooltip-icon.component.ts | 7 +- .../shared/components/hotkeys.directive.ts | 5 +- .../app/shared/components/html.component.ts | 33 +- .../app/shared/components/icon.component.ts | 35 +- .../components/image-input.component.ts | 21 +- .../image/embed-image-dialog.component.ts | 7 +- .../image/gallery-image-input.component.ts | 21 +- .../image/image-dialog.component.ts | 7 +- .../image/image-gallery-dialog.component.ts | 7 +- .../image/image-gallery.component.ts | 9 +- .../image/image-references.component.ts | 7 +- .../multiple-gallery-image-input.component.ts | 21 +- .../image/scada-symbol-input.component.ts | 21 +- .../image/upload-image-dialog.component.ts | 9 +- .../js-func-module-row.component.ts | 33 +- .../components/js-func-modules.component.ts | 9 +- .../shared/components/js-func.component.ts | 33 +- .../components/json-content.component.ts | 33 +- .../components/json-object-edit.component.ts | 33 +- .../components/json-object-view.component.ts | 21 +- .../app/shared/components/kv-map.component.ts | 31 +- .../shared/components/led-light.component.ts | 7 +- .../app/shared/components/logo.component.ts | 7 +- .../components/markdown-editor.component.ts | 23 +- .../shared/components/markdown.component.ts | 7 +- .../material-icon-select.component.ts | 21 +- .../components/material-icons.component.ts | 11 +- .../message-type-autocomplete.component.ts | 17 +- .../mqtt-version-select.component.ts | 17 +- .../multiple-image-input.component.ts | 21 +- .../shared/components/nav-tree.component.ts | 9 +- .../notification/notification.component.ts | 7 +- .../template-autocomplete.component.ts | 19 +- .../ota-package-autocomplete.component.ts | 17 +- ...password-requirements-tooltip.component.ts | 9 +- .../components/phone-input.component.ts | 33 +- .../shared/components/popover.component.ts | 32 +- .../components/protobuf-content.component.ts | 21 +- .../queue/queue-autocomplete.component.ts | 17 +- .../relation-type-autocomplete.component.ts | 17 +- .../resource-autocomplete.component.ts | 17 +- .../resources-in-use-dialog.component.ts | 7 +- .../rule-chain-select-panel.component.ts | 7 +- .../rule-chain/rule-chain-select.component.ts | 17 +- .../components/script-lang.component.ts | 23 +- ...ack-conversation-autocomplete.component.ts | 17 +- .../components/socialshare-panel.component.ts | 7 +- .../string-autocomplete.component.ts | 21 +- .../components/string-items-list.component.ts | 23 +- .../string-pattern-autocomplete.component.ts | 21 +- .../shared/components/svg-xml.component.ts | 33 +- .../shared/components/tb-anchor.component.ts | 5 +- .../components/tb-checkbox.component.ts | 19 +- .../shared/components/tb-error.component.ts | 33 +- .../components/time-unit-input.component.ts | 23 +- ...regation-options-config-panel.component.ts | 7 +- .../aggregation-type-select.component.ts | 17 +- .../grouping-interval-options.component.ts | 21 +- .../time/datapoints-limit.component.ts | 31 +- .../time/datetime-period.component.ts | 21 +- .../components/time/datetime.component.ts | 21 +- .../history-selector.component.ts | 7 +- ...interval-options-config-panel.component.ts | 7 +- .../time/quick-time-interval.component.ts | 21 +- .../components/time/timeinterval.component.ts | 21 +- .../timewindow-config-dialog.component.ts | 7 +- .../time/timewindow-panel.component.ts | 7 +- .../components/time/timewindow.component.ts | 21 +- .../time/timezone-panel.component.ts | 7 +- .../time/timezone-select.component.ts | 17 +- .../components/time/timezone.component.ts | 21 +- .../app/shared/components/toast.directive.ts | 12 +- .../components/toggle-header.component.ts | 10 +- .../components/toggle-select.component.ts | 21 +- .../shared/components/unit-input.component.ts | 23 +- .../unit-settings-panel.component.ts | 11 +- .../shared/components/user-menu.component.ts | 9 +- .../components/value-input.component.ts | 21 +- .../vc/branch-autocomplete.component.ts | 19 +- .../widgets-bundle-search.component.ts | 19 +- .../widgets-bundle-select.component.ts | 19 +- .../directives/context-menu.directive.ts | 3 +- .../truncate-with-tooltip.directive.ts | 11 +- .../export-resource-dialog.component.ts | 7 +- .../import-dialog-csv.component.ts | 9 +- .../import-export/import-dialog.component.ts | 9 +- .../table-columns-assignment.component.ts | 31 +- .../app/shared/pipe/custom-translate.pipe.ts | 3 +- ui-ngx/src/app/shared/pipe/date-ago.pipe.ts | 3 +- .../app/shared/pipe/date-expiration.pipe.ts | 3 +- .../src/app/shared/pipe/enum-to-array.pipe.ts | 3 +- ui-ngx/src/app/shared/pipe/file-size.pipe.ts | 5 +- ui-ngx/src/app/shared/pipe/highlight.pipe.ts | 5 +- ui-ngx/src/app/shared/pipe/image.pipe.ts | 3 +- .../app/shared/pipe/keyboard-shortcut.pipe.ts | 3 +- .../pipe/milliseconds-to-time-string.pipe.ts | 3 +- ui-ngx/src/app/shared/pipe/nospace.pipe.ts | 3 +- ui-ngx/src/app/shared/pipe/safe.pipe.ts | 3 +- .../shared/pipe/selectable-columns.pipe.ts | 5 +- .../src/app/shared/pipe/short-number.pipe.ts | 3 +- ui-ngx/src/app/shared/pipe/tbJson.pipe.ts | 5 +- ui-ngx/src/app/shared/pipe/truncate.pipe.ts | 5 +- ui-ngx/yarn.lock | 3149 ++++++++++++++--- 981 files changed, 9876 insertions(+), 6738 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index c1385e46d2..2cb8ad15cc 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -13,16 +13,16 @@ }, "private": true, "dependencies": { - "@angular/animations": "18.2.13", + "@angular/animations": "19.2.18", "@angular/cdk": "18.2.14", - "@angular/common": "18.2.13", - "@angular/compiler": "18.2.13", - "@angular/core": "18.2.13", - "@angular/forms": "18.2.13", + "@angular/common": "19.2.18", + "@angular/compiler": "19.2.18", + "@angular/core": "19.2.18", + "@angular/forms": "19.2.18", "@angular/material": "18.2.14", - "@angular/platform-browser": "18.2.13", - "@angular/platform-browser-dynamic": "18.2.13", - "@angular/router": "18.2.13", + "@angular/platform-browser": "19.2.18", + "@angular/platform-browser-dynamic": "19.2.18", + "@angular/router": "19.2.18", "@auth0/angular-jwt": "^5.2.0", "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "18.0.1", @@ -65,7 +65,7 @@ "leaflet.markercluster": "1.5.3", "libphonenumber-js": "^1.11.15", "maplibre-gl": "^5.2.0", - "marked": "~12.0.2", + "marked": "~15.0.12", "moment": "^2.30.1", "moment-timezone": "^0.5.45", "ngx-clipboard": "^16.0.0", @@ -73,7 +73,7 @@ "ngx-drag-drop": "^18.0.2", "ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/3.0.0", "ngx-hm-carousel": "^18.0.0", - "ngx-markdown": "^18.1.0", + "ngx-markdown": "^19.1.1", "ngx-sharebuttons": "^15.0.6", "ngx-translate-messageformat-compiler": "^7.0.0", "objectpath": "^2.0.0", @@ -90,17 +90,17 @@ "tooltipster": "^4.2.8", "tslib": "^2.7.0", "typeface-roboto": "^1.1.13", - "zone.js": "~0.14.10" + "zone.js": "~0.15.1" }, "devDependencies": { "@angular-builders/custom-esbuild": "18.0.0", - "@angular-devkit/build-angular": "18.2.12", - "@angular-devkit/core": "18.2.12", - "@angular-devkit/schematics": "18.2.12", - "@angular/build": "18.2.12", - "@angular/cli": "18.2.12", - "@angular/compiler-cli": "18.2.13", - "@angular/language-service": "18.2.13", + "@angular-devkit/build-angular": "19.2.19", + "@angular-devkit/core": "19.2.19", + "@angular-devkit/schematics": "19.2.19", + "@angular/build": "19.2.19", + "@angular/cli": "19.2.19", + "@angular/compiler-cli": "19.2.18", + "@angular/language-service": "19.2.18", "@types/ace-diff": "^2.1.4", "@types/canvas-gauges": "^2.1.8", "@types/flot": "^0.0.36", diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 5bc70240a5..90e01c4c17 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -35,9 +35,10 @@ import { SETTINGS_KEY } from '@core/settings/settings.effects'; import { initCustomJQueryEvents } from '@shared/models/jquery-event.models'; @Component({ - selector: 'tb-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + selector: 'tb-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + standalone: false }) export class AppComponent { diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index 379b8f7dbc..77948333a1 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -47,9 +47,10 @@ export interface AIModelDialogData { } @Component({ - selector: 'tb-ai-model-dialog', - templateUrl: './ai-model-dialog.component.html', - styleUrls: ['./ai-model-dialog.component.scss'] + selector: 'tb-ai-model-dialog', + templateUrl: './ai-model-dialog.component.html', + styleUrls: ['./ai-model-dialog.component.scss'], + standalone: false }) export class AIModelDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts index f16e85983f..3c48890c2f 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts @@ -28,9 +28,10 @@ export interface AIModelDialogData { } @Component({ - selector: 'tb-check-connectivity-dialog', - templateUrl: './check-connectivity-dialog.component.html', - styleUrls: ['./check-connectivity-dialog.component.scss'] + selector: 'tb-check-connectivity-dialog', + templateUrl: './check-connectivity-dialog.component.html', + styleUrls: ['./check-connectivity-dialog.component.scss'], + standalone: false }) export class CheckConnectivityDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts index fc55602e35..5e36be0893 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts @@ -28,10 +28,11 @@ export interface AlarmRuleDetailsDialogData { } @Component({ - selector: 'tb-edit-alarm-details-dialog', - templateUrl: './alarm-rule-details-dialog.component.html', - providers: [], - styleUrls: ['./cf-alarm-rules-dialog.component.scss'], + selector: 'tb-edit-alarm-details-dialog', + templateUrl: './alarm-rule-details-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], + standalone: false }) export class AlarmRuleDetailsDialogComponent extends DialogComponent { 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 2c116aac86..62cee00513 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 @@ -61,10 +61,11 @@ export interface AlarmRuleDialogData { } @Component({ - selector: 'tb-alarm-rule-dialog', - templateUrl: './alarm-rule-dialog.component.html', - styleUrls: ['./alarm-rule-dialog.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-alarm-rule-dialog', + templateUrl: './alarm-rule-dialog.component.html', + styleUrls: ['./alarm-rule-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class AlarmRuleDialogComponent extends DialogComponent { 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 daf0408182..15e92c7ee6 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 @@ -51,16 +51,17 @@ export interface AlarmRuleFilterConfigData { } @Component({ - selector: 'tb-alarm-rule-filter-config', - templateUrl: './alarm-rule-filter-config.component.html', - styleUrls: ['./alarm-rule-filter-config.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmRuleFilterConfigComponent), - multi: true - } - ] + selector: 'tb-alarm-rule-filter-config', + templateUrl: './alarm-rule-filter-config.component.html', + styleUrls: ['./alarm-rule-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterConfigComponent), + multi: true + } + ], + standalone: false }) export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts index 95781decdb..4dc6c6dbb9 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts @@ -22,9 +22,10 @@ import { CalculatedFieldAlarmRule, CalculatedFieldsQuery } from "@shared/models/ import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; @Component({ - selector: 'tb-alarm-rule-table-header', - templateUrl: './alarm-rule-table-header.component.html', - styleUrls: ['./alarm-rule-table-header.component.scss'] + selector: 'tb-alarm-rule-table-header', + templateUrl: './alarm-rule-table-header.component.html', + styleUrls: ['./alarm-rule-table-header.component.scss'], + standalone: false }) export class AlarmRuleTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index 8ae5af0b57..a13aa2fac7 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -39,11 +39,12 @@ import { UtilsService } from "@core/services/utils.service"; import { ActivatedRoute, Router } from "@angular/router"; @Component({ - selector: 'tb-alarm-rules-table', - templateUrl: './alarm-rules-table.component.html', - styleUrls: ['./alarm-rules-table.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [EntityDebugSettingsService] + selector: 'tb-alarm-rules-table', + templateUrl: './alarm-rules-table.component.html', + styleUrls: ['./alarm-rules-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [EntityDebugSettingsService], + standalone: false }) export class AlarmRulesTableComponent { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts index dbb5693d52..13c388675c 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts @@ -51,9 +51,10 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityService } from '@core/http/entity.service'; @Component({ - selector: 'tb-alarm-rules', - templateUrl: './alarm-rules.component.html', - styleUrls: ['./alarm-rule-dialog.component.scss'] + selector: 'tb-alarm-rules', + templateUrl: './alarm-rules.component.html', + styleUrls: ['./alarm-rule-dialog.component.scss'], + standalone: false }) export class AlarmRulesComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts index 946d9db193..49f8e92458 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -53,10 +53,11 @@ export interface CfAlarmRuleConditionDialogData { } @Component({ - selector: 'tb-cf-alarm-rule-condition-dialog', - templateUrl: './cf-alarm-rule-condition-dialog.component.html', - providers: [], - styleUrls: ['./cf-alarm-rules-dialog.component.scss'], + selector: 'tb-cf-alarm-rule-condition-dialog', + templateUrl: './cf-alarm-rule-condition-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], + standalone: false }) export class CfAlarmRuleConditionDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index ae219d4446..fe0670b4e7 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -54,21 +54,22 @@ import { coerceBoolean } from "@shared/decorators/coercion"; import { Observable } from "rxjs"; @Component({ - selector: 'tb-cf-alarm-rule-condition', - templateUrl: './cf-alarm-rule-condition.component.html', - styleUrls: ['./cf-alarm-rule-condition.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CfAlarmRuleConditionComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CfAlarmRuleConditionComponent), - multi: true, - } - ] + selector: 'tb-cf-alarm-rule-condition', + templateUrl: './cf-alarm-rule-condition.component.html', + styleUrls: ['./cf-alarm-rule-condition.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true, + } + ], + standalone: false }) export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts index 1ba769a19e..706477f72a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -38,21 +38,22 @@ import { coerceBoolean } from "@shared/decorators/coercion"; import { Observable } from "rxjs"; @Component({ - selector: 'tb-cf-alarm-rule', - templateUrl: './cf-alarm-rule.component.html', - styleUrls: ['./cf-alarm-rule.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CfAlarmRuleComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CfAlarmRuleComponent), - multi: true, - } - ] + selector: 'tb-cf-alarm-rule', + templateUrl: './cf-alarm-rule.component.html', + styleUrls: ['./cf-alarm-rule.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true, + } + ], + standalone: false }) export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts index 132fbfeed5..d603e9b8b5 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts @@ -31,10 +31,11 @@ export interface AlarmRuleScheduleDialogData { } @Component({ - selector: 'tb-cf-alarm-schedule-dialog', - templateUrl: './cf-alarm-schedule-dialog.component.html', - providers: [], - styleUrls: ['./cf-alarm-rules-dialog.component.scss'], + selector: 'tb-cf-alarm-schedule-dialog', + templateUrl: './cf-alarm-schedule-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], + standalone: false }) export class CfAlarmScheduleDialogComponent extends DialogComponent{ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts index 397e37d2a5..7cc06c37aa 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts @@ -47,18 +47,19 @@ import { MatChipSelectionChange } from "@angular/material/chips"; import { coerceBoolean } from "@shared/decorators/coercion"; @Component({ - selector: 'tb-cf-alarm-schedule', - templateUrl: './cf-alarm-schedule.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CfAlarmScheduleComponent), - multi: true - }, { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CfAlarmScheduleComponent), - multi: true - }] + selector: 'tb-cf-alarm-schedule', + templateUrl: './cf-alarm-schedule.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmScheduleComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmScheduleComponent), + multi: true + }], + standalone: false }) export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { 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 1c56cdbbad..4d9884f851 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 @@ -35,21 +35,22 @@ import { coerceBoolean } from "@shared/decorators/coercion"; import { Observable } from "rxjs"; @Component({ - selector: 'tb-create-cf-alarm-rules', - templateUrl: './create-cf-alarm-rules.component.html', - styleUrls: ['./create-cf-alarm-rules.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CreateCfAlarmRulesComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CreateCfAlarmRulesComponent), - multi: true, - } - ] + selector: 'tb-create-cf-alarm-rules', + templateUrl: './create-cf-alarm-rules.component.html', + styleUrls: ['./create-cf-alarm-rules.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CreateCfAlarmRulesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CreateCfAlarmRulesComponent), + multi: true, + } + ], + standalone: false }) export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts index 017cefb890..951aa1a083 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts @@ -40,10 +40,11 @@ export interface AlarmRuleComplexFilterPredicateDialogData { } @Component({ - selector: 'tb-alarm-rule-complex-filter-predicate-dialog', - templateUrl: './alarm-rule-complex-filter-predicate-dialog.component.html', - providers: [], - styleUrls: [] + selector: 'tb-alarm-rule-complex-filter-predicate-dialog', + templateUrl: './alarm-rule-complex-filter-predicate-dialog.component.html', + providers: [], + styleUrls: [], + standalone: false }) export class AlarmRuleComplexFilterPredicateDialogComponent extends diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts index ba3adc1695..8dcb00daae 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts @@ -43,10 +43,11 @@ } @Component({ - selector: 'tb-alarm-rule-filter-dialog', - templateUrl: './alarm-rule-filter-dialog.component.html', - providers: [], - styleUrls: ['./alarm-rule-filter-dialog.component.scss'] + selector: 'tb-alarm-rule-filter-dialog', + templateUrl: './alarm-rule-filter-dialog.component.html', + providers: [], + styleUrls: ['./alarm-rule-filter-dialog.component.scss'], + standalone: false }) export class AlarmRuleFilterDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts index 5bb05ab1eb..706e2c4a86 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts @@ -44,21 +44,22 @@ import { CalculatedFieldArgument } from "@shared/models/calculated-field.models" import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @Component({ - selector: 'tb-alarm-rule-filter-list', - templateUrl: './alarm-rule-filter-list.component.html', - styleUrls: ['./alarm-rule-filter-list.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmRuleFilterListComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AlarmRuleFilterListComponent), - multi: true - } - ] + selector: 'tb-alarm-rule-filter-list', + templateUrl: './alarm-rule-filter-list.component.html', + styleUrls: ['./alarm-rule-filter-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterListComponent), + multi: true + } + ], + standalone: false }) export class AlarmRuleFilterListComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts index 7bf08c9ac9..12fce94fb3 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts @@ -52,21 +52,22 @@ import { CalculatedFieldArgument } from "@shared/models/calculated-field.models" import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @Component({ - selector: 'tb-alarm-rule-filter-predicate-list', - templateUrl: './alarm-rule-filter-predicate-list.component.html', - styleUrls: ['./alarm-rule-filter-predicate-list.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), - multi: true - } - ] + selector: 'tb-alarm-rule-filter-predicate-list', + templateUrl: './alarm-rule-filter-predicate-list.component.html', + styleUrls: ['./alarm-rule-filter-predicate-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), + multi: true + } + ], + standalone: false }) export class AlarmRuleFilterPredicateListComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts index 7f9608bd5c..977bb5749f 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts @@ -31,21 +31,22 @@ import { AlarmRuleFilterPredicateType, NoDataAlarmRuleFilterPredicate } from "@s import { isDefinedAndNotNull } from "@core/utils"; @Component({ - selector: 'tb-alarm-rule-filter-predicate-no-data-value', - templateUrl: './alarm-rule-filter-predicate-no-data-value.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent), - multi: true - } - ] + selector: 'tb-alarm-rule-filter-predicate-no-data-value', + templateUrl: './alarm-rule-filter-predicate-no-data-value.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent), + multi: true + } + ], + standalone: false }) export class AlarmRuleFilterPredicateNoDataValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts index bd6bacb762..8db5bc7325 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts @@ -34,21 +34,22 @@ import { FormControlsFrom } from "@shared/models/tenant.model"; import { isDefinedAndNotNull } from "@core/utils"; @Component({ - selector: 'tb-alarm-rule-filter-predicate-value', - templateUrl: './alarm-rule-filter-predicate-value.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), - multi: true - } - ] + selector: 'tb-alarm-rule-filter-predicate-value', + templateUrl: './alarm-rule-filter-predicate-value.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), + multi: true + } + ], + standalone: false }) export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts index 77cc962747..c458cd64ed 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts @@ -45,21 +45,22 @@ import { import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; @Component({ - selector: 'tb-alarm-rule-filter-predicate', - templateUrl: './alarm-rule-filter-predicate.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), - multi: true - } - ] + selector: 'tb-alarm-rule-filter-predicate', + templateUrl: './alarm-rule-filter-predicate.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), + multi: true + } + ], + standalone: false }) export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts index 7bb8bada5d..0758b09e38 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts @@ -39,10 +39,11 @@ import { coerceBoolean } from "@shared/decorators/coercion"; import { timeUnitTranslationMap } from "@shared/models/time/time.models"; @Component({ - selector: 'tb-alarm-rule-filter-text', - templateUrl: './alarm-rule-filter-text.component.html', - styleUrls: ['./alarm-rule-filter-text.component.scss'], - providers: [] + selector: 'tb-alarm-rule-filter-text', + templateUrl: './alarm-rule-filter-text.component.html', + styleUrls: ['./alarm-rule-filter-text.component.scss'], + providers: [], + standalone: false }) export class AlarmRuleFilterTextComponent { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts index 4dedace9cc..ddc46edcb8 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts @@ -54,9 +54,10 @@ export interface AlarmAssigneePanelData { } @Component({ - selector: 'tb-alarm-assignee-panel', - templateUrl: './alarm-assignee-panel.component.html', - styleUrls: ['./alarm-assignee-panel.component.scss'] + selector: 'tb-alarm-assignee-panel', + templateUrl: './alarm-assignee-panel.component.html', + styleUrls: ['./alarm-assignee-panel.component.scss'], + standalone: false }) export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts index 7cba10aa2f..845d3a8089 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts @@ -47,9 +47,10 @@ export interface AlarmAssigneeSelectPanelData { } @Component({ - selector: 'tb-alarm-assignee-select-panel', - templateUrl: './alarm-assignee-panel.component.html', - styleUrls: ['./alarm-assignee-panel.component.scss'] + selector: 'tb-alarm-assignee-select-panel', + templateUrl: './alarm-assignee-panel.component.html', + styleUrls: ['./alarm-assignee-panel.component.scss'], + standalone: false }) export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts index dd0ebce70b..a97a4a04ac 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts @@ -34,16 +34,17 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { AlarmAssigneeOption } from '@shared/models/alarm.models'; @Component({ - selector: 'tb-alarm-assignee-select', - templateUrl: './alarm-assignee-select.component.html', - styleUrls: ['./alarm-assignee.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmAssigneeSelectComponent), - multi: true - } - ] + selector: 'tb-alarm-assignee-select', + templateUrl: './alarm-assignee-select.component.html', + styleUrls: ['./alarm-assignee.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmAssigneeSelectComponent), + multi: true + } + ], + standalone: false }) export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts index 604685d76e..55127e68c5 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts @@ -28,9 +28,10 @@ import { TranslateService } from '@ngx-translate/core'; import { isNotEmptyStr } from '@core/utils'; @Component({ - selector: 'tb-alarm-assignee', - templateUrl: './alarm-assignee.component.html', - styleUrls: ['./alarm-assignee.component.scss'] + selector: 'tb-alarm-assignee', + templateUrl: './alarm-assignee.component.html', + styleUrls: ['./alarm-assignee.component.scss'], + standalone: false }) export class AlarmAssigneeComponent { @Input() diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts index 0584c4d4e4..8c9b751348 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts @@ -28,9 +28,10 @@ export interface AlarmCommentDialogData { } @Component({ - selector: 'tb-alarm-comment-dialog', - templateUrl: './alarm-comment-dialog.component.html', - styleUrls: [] + selector: 'tb-alarm-comment-dialog', + templateUrl: './alarm-comment-dialog.component.html', + styleUrls: [], + standalone: false }) export class AlarmCommentDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts index d35e87fdbe..cce31840ab 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts @@ -59,9 +59,10 @@ interface AlarmCommentsDisplayData { } @Component({ - selector: 'tb-alarm-comment', - templateUrl: './alarm-comment.component.html', - styleUrls: ['./alarm-comment.component.scss'] + selector: 'tb-alarm-comment', + templateUrl: './alarm-comment.component.html', + styleUrls: ['./alarm-comment.component.scss'], + standalone: false }) export class AlarmCommentComponent implements OnInit { @Input() diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts index b68d1d983a..67d2b71245 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts @@ -47,9 +47,10 @@ export interface AlarmDetailsDialogData { } @Component({ - selector: 'tb-alarm-details-dialog', - templateUrl: './alarm-details-dialog.component.html', - styleUrls: ['./alarm-details-dialog.component.scss'] + selector: 'tb-alarm-details-dialog', + templateUrl: './alarm-details-dialog.component.html', + styleUrls: ['./alarm-details-dialog.component.scss'], + standalone: false }) export class AlarmDetailsDialogComponent extends DialogComponent { 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 c36d528b4e..f236873806 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 @@ -60,16 +60,17 @@ export interface AlarmFilterConfigData { // @dynamic @Component({ - selector: 'tb-alarm-filter-config', - templateUrl: './alarm-filter-config.component.html', - styleUrls: ['./alarm-filter-config.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmFilterConfigComponent), - multi: true - } - ] + selector: 'tb-alarm-filter-config', + templateUrl: './alarm-filter-config.component.html', + styleUrls: ['./alarm-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmFilterConfigComponent), + multi: true + } + ], + standalone: false }) export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts index c069169e4a..7805498f1c 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts @@ -23,9 +23,10 @@ import { AlarmTableConfig } from './alarm-table-config'; import { AlarmFilterConfig } from '@shared/models/query/query.models'; @Component({ - selector: 'tb-alarm-table-header', - templateUrl: './alarm-table-header.component.html', - styleUrls: ['./alarm-table-header.component.scss'] + selector: 'tb-alarm-table-header', + templateUrl: './alarm-table-header.component.html', + styleUrls: ['./alarm-table-header.component.scss'], + standalone: false }) export class AlarmTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts index daa28d0fd9..c0754b3a94 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts @@ -42,9 +42,10 @@ interface AlarmPageQueryParams extends PageQueryParam { } @Component({ - selector: 'tb-alarm-table', - templateUrl: './alarm-table.component.html', - styleUrls: ['./alarm-table.component.scss'] + selector: 'tb-alarm-table', + templateUrl: './alarm-table.component.html', + styleUrls: ['./alarm-table.component.scss'], + standalone: false }) export class AlarmTableComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts index c1f3938853..89bdc9c8cc 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts @@ -29,14 +29,15 @@ import { EntityService } from '@core/http/entity.service'; import { isDefinedAndNotNull } from '@core/utils'; @Component({ - selector: 'tb-aliases-entity-autocomplete', - templateUrl: './aliases-entity-autocomplete.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AliasesEntityAutocompleteComponent), - multi: true - }] + selector: 'tb-aliases-entity-autocomplete', + templateUrl: './aliases-entity-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AliasesEntityAutocompleteComponent), + multi: true + }], + standalone: false }) export class AliasesEntityAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts index 9d6c84b3ff..89df2ee017 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts @@ -26,9 +26,10 @@ export interface AliasesEntitySelectPanelData { } @Component({ - selector: 'tb-aliases-entity-select-panel', - templateUrl: './aliases-entity-select-panel.component.html', - styleUrls: ['./aliases-entity-select-panel.component.scss'] + selector: 'tb-aliases-entity-select-panel', + templateUrl: './aliases-entity-select-panel.component.html', + styleUrls: ['./aliases-entity-select-panel.component.scss'], + standalone: false }) export class AliasesEntitySelectPanelComponent { diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts index 74f4a8c866..a8e1ddf762 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts @@ -41,9 +41,10 @@ import { deepClone } from '@core/utils'; import { AliasFilterType } from '@shared/models/alias.models'; @Component({ - selector: 'tb-aliases-entity-select', - templateUrl: './aliases-entity-select.component.html', - styleUrls: ['./aliases-entity-select.component.scss'] + selector: 'tb-aliases-entity-select', + templateUrl: './aliases-entity-select.component.html', + styleUrls: ['./aliases-entity-select.component.scss'], + standalone: false }) export class AliasesEntitySelectComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts index 5fc1e4baa1..208aabab3c 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts @@ -46,10 +46,11 @@ export interface EntityAliasDialogData { } @Component({ - selector: 'tb-entity-alias-dialog', - templateUrl: './entity-alias-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasDialogComponent}], - styleUrls: ['./entity-alias-dialog.component.scss'] + selector: 'tb-entity-alias-dialog', + templateUrl: './entity-alias-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: EntityAliasDialogComponent }], + styleUrls: ['./entity-alias-dialog.component.scss'], + standalone: false }) export class EntityAliasDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts index a1a430670e..62f325d8ae 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -54,10 +54,11 @@ export interface EntityAliasesDialogData { } @Component({ - selector: 'tb-entity-aliases-dialog', - templateUrl: './entity-aliases-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasesDialogComponent}], - styleUrls: ['./entity-aliases-dialog.component.scss'] + selector: 'tb-entity-aliases-dialog', + templateUrl: './entity-aliases-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: EntityAliasesDialogComponent }], + styleUrls: ['./entity-aliases-dialog.component.scss'], + standalone: false }) export class EntityAliasesDialogComponent extends DialogComponent implements ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.ts b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.ts index b51d7607b9..1ac2fa10ac 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.ts @@ -29,9 +29,10 @@ import { ApiKeysTableDialogData } from '@home/components/api-key/api-keys-table- import { DAY } from '@shared/models/time/time.models'; @Component({ - selector: 'tb-add-api-key-dialog', - templateUrl: './add-api-key-dialog.component.html', - styleUrls: ['./add-api-key-dialog.component.scss'] + selector: 'tb-add-api-key-dialog', + templateUrl: './add-api-key-dialog.component.html', + styleUrls: ['./add-api-key-dialog.component.scss'], + standalone: false }) export class AddApiKeyDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts b/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts index a6fed8376e..d080d16451 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts @@ -28,9 +28,10 @@ export interface ApiKeyGeneratedDialogData { } @Component({ - selector: 'tb-api-key-generated-dialog', - templateUrl: './api-key-generated-dialog.component.html', - styleUrls: ['api-key-generated-dialog.component.scss'] + selector: 'tb-api-key-generated-dialog', + templateUrl: './api-key-generated-dialog.component.html', + styleUrls: ['api-key-generated-dialog.component.scss'], + standalone: false }) export class ApiKeyGeneratedDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.ts b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.ts index aaba7b9f1b..208e1ac646 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-dialog.component.ts @@ -27,9 +27,10 @@ export interface ApiKeysTableDialogData { } @Component({ - selector: 'tb-api-keys-table-dialog', - templateUrl: './api-keys-table-dialog.component.html', - styleUrls: ['api-keys-table-dialog.component.scss'] + selector: 'tb-api-keys-table-dialog', + templateUrl: './api-keys-table-dialog.component.html', + styleUrls: ['api-keys-table-dialog.component.scss'], + standalone: false }) export class ApiKeysTableDialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.ts b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.ts index 18730803a4..b27da1456e 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.ts @@ -35,10 +35,11 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { UserId } from '@shared/models/id/user-id'; @Component({ - selector: 'tb-api-keys-table', - templateUrl: './api-keys-table.component.html', - styleUrls: ['./api-keys-table.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-api-keys-table', + templateUrl: './api-keys-table.component.html', + styleUrls: ['./api-keys-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class ApiKeysTableComponent { diff --git a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts index 1614ea1ed3..8f05a755f4 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts @@ -21,10 +21,11 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { ApiKeyService } from '@core/http/api-key.service'; @Component({ - selector: 'tb-edit-api-key-description-panel', - templateUrl: './edit-api-key-description-panel.component.html', - styleUrls: ['./edit-api-key-description-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-edit-api-key-description-panel', + templateUrl: './edit-api-key-description-panel.component.html', + styleUrls: ['./edit-api-key-description-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class EditApiKeyDescriptionPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts index 056b155e20..9c20df523f 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts @@ -43,10 +43,11 @@ export interface AddAttributeDialogData { } @Component({ - selector: 'tb-add-attribute-dialog', - templateUrl: './add-attribute-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AddAttributeDialogComponent}], - styleUrls: [] + selector: 'tb-add-attribute-dialog', + templateUrl: './add-attribute-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AddAttributeDialogComponent }], + styleUrls: [], + standalone: false }) export class AddAttributeDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts index 95e51ac915..c03b7a71f2 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts @@ -49,10 +49,11 @@ export interface AddWidgetToDashboardDialogData { } @Component({ - selector: 'tb-add-widget-to-dashboard-dialog', - templateUrl: './add-widget-to-dashboard-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetToDashboardDialogComponent}], - styleUrls: ['./add-widget-to-dashboard-dialog.component.scss'] + selector: 'tb-add-widget-to-dashboard-dialog', + templateUrl: './add-widget-to-dashboard-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AddWidgetToDashboardDialogComponent }], + styleUrls: ['./add-widget-to-dashboard-dialog.component.scss'], + standalone: false }) export class AddWidgetToDashboardDialogComponent extends DialogComponent diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index 94ad550c33..9187b69b73 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -93,10 +93,11 @@ import { TimeService } from '@core/services/time.service'; @Component({ - selector: 'tb-attribute-table', - templateUrl: './attribute-table.component.html', - styleUrls: ['./attribute-table.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-attribute-table', + templateUrl: './attribute-table.component.html', + styleUrls: ['./attribute-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class AttributeTableComponent extends PageComponent implements AfterViewInit, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.ts b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.ts index 5b24eea5a9..c7d68f04e0 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.ts @@ -36,9 +36,10 @@ export interface DeleteTimeseriesPanelResult { } @Component({ - selector: 'tb-delete-timeseries-panel', - templateUrl: './delete-timeseries-panel.component.html', - styleUrls: ['./delete-timeseries-panel.component.scss'] + selector: 'tb-delete-timeseries-panel', + templateUrl: './delete-timeseries-panel.component.html', + styleUrls: ['./delete-timeseries-panel.component.scss'], + standalone: false }) export class DeleteTimeseriesPanelComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts index 0eca716bb9..dbe63cb2b3 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts @@ -29,10 +29,11 @@ export interface EditAttributeValuePanelData { } @Component({ - selector: 'tb-edit-attribute-value-panel', - templateUrl: './edit-attribute-value-panel.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: EditAttributeValuePanelComponent}], - styleUrls: ['./edit-attribute-value-panel.component.scss'] + selector: 'tb-edit-attribute-value-panel', + templateUrl: './edit-attribute-value-panel.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: EditAttributeValuePanelComponent }], + styleUrls: ['./edit-attribute-value-panel.component.scss'], + standalone: false }) export class EditAttributeValuePanelComponent extends PageComponent implements OnInit, ErrorStateMatcher { 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 17675a913a..eeb976ebf8 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 @@ -33,9 +33,10 @@ export interface AuditLogDetailsDialogData { } @Component({ - selector: 'tb-audit-log-details-dialog', - templateUrl: './audit-log-details-dialog.component.html', - styleUrls: ['./audit-log-details-dialog.component.scss'] + selector: 'tb-audit-log-details-dialog', + templateUrl: './audit-log-details-dialog.component.html', + styleUrls: ['./audit-log-details-dialog.component.scss'], + standalone: false }) export class AuditLogDetailsDialogComponent extends DialogComponent implements OnInit, OnDestroy { 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 98e76c6fc7..631a795f7d 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 @@ -43,16 +43,17 @@ import { TranslateService } from '@ngx-translate/core'; // @dynamic @Component({ - selector: 'tb-audit-log-filter', - templateUrl: './audit-log-filter.component.html', - styleUrls: ['./audit-log-filter.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AuditLogFilterComponent), - multi: true - } - ] + selector: 'tb-audit-log-filter', + templateUrl: './audit-log-filter.component.html', + styleUrls: ['./audit-log-filter.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AuditLogFilterComponent), + multi: true + } + ], + standalone: false }) export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts index 8b8009a838..1fdc68cdfa 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts @@ -21,9 +21,10 @@ import { EntityTableHeaderComponent } from '@home/components/entity/entity-table import { Store } from '@ngrx/store'; @Component({ - selector: 'tb-audit-log-header', - templateUrl: './audit-log-header.component.html', - styles: `` + selector: 'tb-audit-log-header', + templateUrl: './audit-log-header.component.html', + styles: ``, + standalone: false }) export class AuditLogHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts index 71e845502a..8a912348be 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts @@ -32,9 +32,10 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { ActivatedRoute } from '@angular/router'; @Component({ - selector: 'tb-audit-log-table', - templateUrl: './audit-log-table.component.html', - styleUrls: ['./audit-log-table.component.scss'] + selector: 'tb-audit-log-table', + templateUrl: './audit-log-table.component.html', + styleUrls: ['./audit-log-table.component.scss'], + standalone: false }) export class AuditLogTableComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts index 425069239f..4790789080 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts @@ -45,9 +45,10 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityService } from '@core/http/entity.service'; @Component({ - selector: 'tb-calculated-field', - templateUrl: './calculated-field.component.html', - styleUrls: ['./calculated-field.component.scss'] + selector: 'tb-calculated-field', + templateUrl: './calculated-field.component.html', + styleUrls: ['./calculated-field.component.scss'], + standalone: false }) export class CalculatedFieldComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 15e622c468..feba407f52 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -39,11 +39,12 @@ import { UtilsService } from "@core/services/utils.service"; import { ActivatedRoute, Router } from '@angular/router'; @Component({ - selector: 'tb-calculated-fields-table', - templateUrl: './calculated-fields-table.component.html', - styleUrls: ['./calculated-fields-table.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [EntityDebugSettingsService] + selector: 'tb-calculated-fields-table', + templateUrl: './calculated-fields-table.component.html', + styleUrls: ['./calculated-fields-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [EntityDebugSettingsService], + standalone: false }) export class CalculatedFieldsTableComponent { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index 37362a29ee..c4d4552999 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -57,9 +57,10 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { TenantId } from '@shared/models/id/tenant-id'; @Component({ - selector: 'tb-calculated-field-argument-panel', - templateUrl: './calculated-field-argument-panel.component.html', - styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-argument-panel.component.scss'] + selector: 'tb-calculated-field-argument-panel', + templateUrl: './calculated-field-argument-panel.component.html', + styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-argument-panel.component.scss'], + standalone: false }) export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit { 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 bd8292d113..4f1aebc5f1 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 @@ -65,21 +65,22 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { BaseData } from '@shared/models/base-data'; @Component({ - selector: 'tb-calculated-field-arguments-table', - templateUrl: './calculated-field-arguments-table.component.html', - styleUrls: [`calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), - multi: true - } - ], + selector: 'tb-calculated-field-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), + multi: true + } + ], + standalone: false }) export class CalculatedFieldArgumentsTableComponent implements ControlValueAccessor, Validator, OnChanges, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts index b371653b18..62dcff420e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts @@ -26,21 +26,22 @@ import { import { ArgumentEntityType } from '@shared/models/calculated-field.models'; @Component({ - selector: 'tb-entity-aggregation-arguments-table', - templateUrl: './calculated-field-arguments-table.component.html', - styleUrls: [`calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), - multi: true - } - ], + selector: 'tb-entity-aggregation-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), + multi: true + } + ], + standalone: false }) export class EntityAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts index b84ea2ca9f..b7208d0cbb 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts @@ -41,21 +41,22 @@ import { isDefined, isUndefinedOrNull } from '@core/utils'; import { NULL_UUID } from '@shared/models/id/has-uuid'; @Component({ - selector: 'tb-propagate-arguments-table', - templateUrl: './calculated-field-arguments-table.component.html', - styleUrls: [`calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => PropagateArgumentsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => PropagateArgumentsTableComponent), - multi: true - } - ], + selector: 'tb-propagate-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PropagateArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PropagateArgumentsTableComponent), + multi: true + } + ], + standalone: false }) export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts index 0a50a82729..52650bbf1c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts @@ -36,21 +36,22 @@ import { AliasFilterType } from '@shared/models/alias.models'; import { EntityType } from '@shared/models/entity-type.models'; @Component({ - selector: 'tb-related-aggregation-arguments-table', - templateUrl: './calculated-field-arguments-table.component.html', - styleUrls: [`calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), - multi: true - } - ], + selector: 'tb-related-aggregation-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), + multi: true + } + ], + standalone: false }) export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { 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 6e8b7fb93f..4e37e67c71 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 @@ -57,10 +57,11 @@ export interface CalculatedFieldDialogData { } @Component({ - selector: 'tb-calculated-field-dialog', - templateUrl: './calculated-field-dialog.component.html', - styleUrls: ['./calculated-field-dialog.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-calculated-field-dialog', + templateUrl: './calculated-field-dialog.component.html', + styleUrls: ['./calculated-field-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class CalculatedFieldDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts index 11c95352d3..8386555d2b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts @@ -59,20 +59,21 @@ enum TimeCategory { } @Component({ - selector: 'tb-entity-aggregation-component', - templateUrl: './entity-aggregation-component.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityAggregationComponentComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => EntityAggregationComponentComponent), - multi: true - } - ], + selector: 'tb-entity-aggregation-component', + templateUrl: './entity-aggregation-component.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + } + ], + standalone: false }) export class EntityAggregationComponentComponent implements ControlValueAccessor, Validator { 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 fc70058f8b..c19dcefcd6 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 @@ -58,9 +58,10 @@ import { EntitySearchDirection } from '@shared/models/relation.models'; import { CdkDragDrop } from "@angular/cdk/drag-drop"; @Component({ - selector: 'tb-calculated-field-geofencing-zone-groups-panel', - templateUrl: './calculated-field-geofencing-zone-groups-panel.component.html', - styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-geofencing-zone-groups-panel.component.scss'] + selector: 'tb-calculated-field-geofencing-zone-groups-panel', + templateUrl: './calculated-field-geofencing-zone-groups-panel.component.html', + styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-geofencing-zone-groups-panel.component.scss'], + standalone: false }) export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit, AfterViewInit { 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 341b898265..52bf631a1f 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 @@ -61,21 +61,22 @@ import { } from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component'; @Component({ - selector: 'tb-calculated-field-geofencing-zone-groups-table', - templateUrl: './calculated-field-geofencing-zone-groups-table.component.html', - styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), - multi: true - } - ], + selector: 'tb-calculated-field-geofencing-zone-groups-table', + templateUrl: './calculated-field-geofencing-zone-groups-table.component.html', + styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + } + ], + standalone: false }) export class CalculatedFieldGeofencingZoneGroupsTableComponent implements ControlValueAccessor, Validator, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts index 02d02241b4..2f621e2a38 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts @@ -43,20 +43,21 @@ import { EntityFilter } from '@shared/models/query/query.models'; import { EntityId } from '@shared/models/id/entity-id'; @Component({ - selector: 'tb-geofencing-configuration', - templateUrl: './geofencing-configuration.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => GeofencingConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => GeofencingConfigurationComponent), - multi: true - } - ], + selector: 'tb-geofencing-configuration', + templateUrl: './geofencing-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GeofencingConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GeofencingConfigurationComponent), + multi: true + } + ], + standalone: false }) export class GeofencingConfigurationComponent implements ControlValueAccessor, Validator, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts index 4f98a7225f..7af34df61c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts @@ -42,9 +42,10 @@ interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricVal } @Component({ - selector: 'tb-calculated-field-metrics-panel', - templateUrl: './calculated-field-metrics-panel.component.html', - styleUrls: ['../common/calculated-field-panel.scss', '../../calculated-field.component.scss'], + selector: 'tb-calculated-field-metrics-panel', + templateUrl: './calculated-field-metrics-panel.component.html', + styleUrls: ['../common/calculated-field-panel.scss', '../../calculated-field.component.scss'], + standalone: false }) export class CalculatedFieldMetricsPanelComponent implements OnInit { 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 7862de3a33..666f33a8aa 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 @@ -59,21 +59,22 @@ import { AceHighlightRules } from '@shared/models/ace/ace.models'; import { Observable } from "rxjs"; @Component({ - selector: 'tb-calculated-field-metrics-table', - templateUrl: './calculated-field-metrics-table.component.html', - styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), - multi: true - } - ], + selector: 'tb-calculated-field-metrics-table', + templateUrl: './calculated-field-metrics-table.component.html', + styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + } + ], + standalone: false }) export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValueAccessor, Validator, AfterViewInit { 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 a9924909b7..0fe3415ac1 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 @@ -42,21 +42,22 @@ import { merge } from 'rxjs'; import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-calculate-field-output', - templateUrl: './calculated-field-output.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CalculatedFieldOutputComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CalculatedFieldOutputComponent), - multi: true - } - ], - styleUrls: ['./calculated-field-output.component.scss'], + selector: 'tb-calculate-field-output', + templateUrl: './calculated-field-output.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldOutputComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldOutputComponent), + multi: true + } + ], + styleUrls: ['./calculated-field-output.component.scss'], + standalone: false }) export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { 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 1cf2476c2e..0db1c21c3b 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 @@ -48,20 +48,21 @@ import {AppState} from "@core/core.state"; import {getCurrentAuthState} from "@core/auth/auth.selectors"; @Component({ - selector: 'tb-propagation-configuration', - templateUrl: './propagation-configuration.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => PropagationConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => PropagationConfigurationComponent), - multi: true - } - ], + selector: 'tb-propagation-configuration', + templateUrl: './propagation-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + } + ], + standalone: false }) export class PropagationConfigurationComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts index fcbe46be03..907bb31abb 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts @@ -46,21 +46,22 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-related-entities-aggregation-component', - templateUrl: './related-entities-aggregation-component.component.html', - styleUrl: './related-entities-aggregation-component.component.scss', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), - multi: true - } - ], + selector: 'tb-related-entities-aggregation-component', + templateUrl: './related-entities-aggregation-component.component.html', + styleUrl: './related-entities-aggregation-component.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + } + ], + standalone: false }) export class RelatedEntitiesAggregationComponentComponent implements ControlValueAccessor, Validator { 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 7292775055..f2f9fbac2f 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 @@ -46,20 +46,21 @@ import { map } from 'rxjs/operators'; type SimpeConfiguration = CalculatedFieldSimpleConfiguration | CalculatedFieldScriptConfiguration; @Component({ - selector: 'tb-simple-configuration', - templateUrl: './simple-configuration.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SimpleConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => SimpleConfigurationComponent), - multi: true - } - ], + selector: 'tb-simple-configuration', + templateUrl: './simple-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + } + ], + standalone: false }) export class SimpleConfigurationComponent implements ControlValueAccessor, Validator, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts index 09b8b5e2cf..9f67906ce7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts @@ -44,21 +44,22 @@ import { filter } from 'rxjs/operators'; import { MatDialog } from '@angular/material/dialog'; @Component({ - selector: 'tb-calculated-field-test-arguments', - templateUrl: './calculated-field-test-arguments.component.html', - styleUrls: ['./calculated-field-test-arguments.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CalculatedFieldTestArgumentsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CalculatedFieldTestArgumentsComponent), - multi: true, - } - ] + selector: 'tb-calculated-field-test-arguments', + templateUrl: './calculated-field-test-arguments.component.html', + styleUrls: ['./calculated-field-test-arguments.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldTestArgumentsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldTestArgumentsComponent), + multi: true, + } + ], + standalone: false }) export class CalculatedFieldTestArgumentsComponent extends PageComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts index 8cf7515e3c..2593ab503b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts @@ -55,10 +55,11 @@ export interface CalculatedFieldTestScriptDialogData extends CalculatedFieldTest } @Component({ - selector: 'tb-calculated-field-script-test-dialog', - templateUrl: './calculated-field-script-test-dialog.component.html', - styleUrls: ['./calculated-field-script-test-dialog.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-calculated-field-script-test-dialog', + templateUrl: './calculated-field-script-test-dialog.component.html', + styleUrls: ['./calculated-field-script-test-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class CalculatedFieldScriptTestDialogComponent extends DialogComponent implements AfterViewInit, OnDestroy { 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 333e74d195..36095579e8 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 @@ -56,16 +56,17 @@ export interface CalculatedFieldsFilterConfigData { } @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 - } - ] + 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 + } + ], + standalone: false }) export class CalculatedFieldsFilterConfigComponent implements OnInit, ControlValueAccessor { 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 index b6bc46371c..07ed7deeda 100644 --- 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 @@ -22,9 +22,10 @@ import { CalculatedField, CalculatedFieldsQuery } from "@shared/models/calculate 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'] + selector: 'tb-calculated-fields-table-header', + templateUrl: './calculated-fields-header.component.html', + styleUrls: ['./calculated-fields-header.component.scss'], + standalone: false }) export class CalculatedFieldsHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 0a4f527064..cc1ef8aed1 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -42,11 +42,12 @@ export interface AddWidgetDialogData { } @Component({ - selector: 'tb-add-widget-dialog', - templateUrl: './add-widget-dialog.component.html', - providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/], - styleUrls: ['./add-widget-dialog.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-add-widget-dialog', + templateUrl: './add-widget-dialog.component.html', + providers: [ /*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/], + styleUrls: ['./add-widget-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class AddWidgetDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.ts index ed17125e0e..571629789b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.ts @@ -41,9 +41,10 @@ export interface DashboardImageDialogResult { } @Component({ - selector: 'tb-dashboard-image-dialog', - templateUrl: './dashboard-image-dialog.component.html', - styleUrls: ['./dashboard-image-dialog.component.scss'] + selector: 'tb-dashboard-image-dialog', + templateUrl: './dashboard-image-dialog.component.html', + styleUrls: ['./dashboard-image-dialog.component.scss'], + standalone: false }) export class DashboardImageDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 84d9061b26..d3e4377b0a 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -160,11 +160,12 @@ import { HttpStatusCode } from '@angular/common/http'; // @dynamic @Component({ - selector: 'tb-dashboard-page', - templateUrl: './dashboard-page.component.html', - styleUrls: ['./dashboard-page.component.scss'], - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-dashboard-page', + templateUrl: './dashboard-page.component.html', + styleUrls: ['./dashboard-page.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class DashboardPageComponent extends PageComponent implements IDashboardController, HasDirtyFlag, OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts index 2675706495..b9d7e61517 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts @@ -47,10 +47,11 @@ export interface DashboardSettingsDialogData { } @Component({ - selector: 'tb-dashboard-settings-dialog', - templateUrl: './dashboard-settings-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: DashboardSettingsDialogComponent}], - styleUrls: ['./dashboard-settings-dialog.component.scss'] + selector: 'tb-dashboard-settings-dialog', + templateUrl: './dashboard-settings-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: DashboardSettingsDialogComponent }], + styleUrls: ['./dashboard-settings-dialog.component.scss'], + standalone: false }) export class DashboardSettingsDialogComponent extends DialogComponent implements OnDestroy, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts index e83ef81e26..7e10544ffa 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts @@ -27,9 +27,10 @@ import { EntityId } from '@shared/models/id/entity-id'; import { Subscription } from 'rxjs'; @Component({ - selector: 'tb-dashboard-state', - templateUrl: './dashboard-state.component.html', - styleUrls: ['./dashboard-state.component.scss'] + selector: 'tb-dashboard-state', + templateUrl: './dashboard-state.component.html', + styleUrls: ['./dashboard-state.component.scss'], + standalone: false }) export class DashboardStateComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.ts index 13961e4c54..fa02811fdb 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.ts @@ -17,10 +17,11 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; @Component({ - selector: 'tb-dashboard-toolbar', - templateUrl: './dashboard-toolbar.component.html', - styleUrls: ['./dashboard-toolbar.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-dashboard-toolbar', + templateUrl: './dashboard-toolbar.component.html', + styleUrls: ['./dashboard-toolbar.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DashboardToolbarComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts index 5ae629a003..bf4a7b78eb 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts @@ -48,10 +48,11 @@ interface BundleWidgetsFilter extends WidgetsFilter { } @Component({ - selector: 'tb-dashboard-widget-select', - templateUrl: './dashboard-widget-select.component.html', - styleUrls: ['./dashboard-widget-select.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-dashboard-widget-select', + templateUrl: './dashboard-widget-select.component.html', + styleUrls: ['./dashboard-widget-select.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DashboardWidgetSelectComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts index 1bb15be0e3..e7d0491efd 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts @@ -31,9 +31,10 @@ import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/co import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-edit-widget', - templateUrl: './edit-widget.component.html', - styleUrls: ['./edit-widget.component.scss'] + selector: 'tb-edit-widget', + templateUrl: './edit-widget.component.html', + styleUrls: ['./edit-widget.component.scss'], + standalone: false }) export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.ts index 08f577a045..94c068da8e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.ts @@ -35,8 +35,9 @@ export interface AddNewBreakpointDialogResult { } @Component({ - selector: 'add-new-breakpoint-dialog', - templateUrl: './add-new-breakpoint-dialog.component.html', + selector: 'add-new-breakpoint-dialog', + templateUrl: './add-new-breakpoint-dialog.component.html', + standalone: false }) export class AddNewBreakpointDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts index 36ec78e3a7..1699fe6c54 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts @@ -42,9 +42,10 @@ import { isNotEmptyStr } from '@core/utils'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; @Component({ - selector: 'tb-dashboard-layout', - templateUrl: './dashboard-layout.component.html', - styleUrls: ['./dashboard-layout.component.scss'] + selector: 'tb-dashboard-layout', + templateUrl: './dashboard-layout.component.html', + styleUrls: ['./dashboard-layout.component.scss'], + standalone: false }) export class DashboardLayoutComponent extends PageComponent implements ILayoutController, DashboardCallbacks, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts index fe7d68eba6..20bded3dd0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts @@ -75,10 +75,11 @@ export interface DashboardLayoutSettings { } @Component({ - selector: 'tb-manage-dashboard-layouts-dialog', - templateUrl: './manage-dashboard-layouts-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardLayoutsDialogComponent}], - styleUrls: ['./manage-dashboard-layouts-dialog.component.scss', '../../../components/dashboard/layout-button.scss'] + selector: 'tb-manage-dashboard-layouts-dialog', + templateUrl: './manage-dashboard-layouts-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: ManageDashboardLayoutsDialogComponent }], + styleUrls: ['./manage-dashboard-layouts-dialog.component.scss', '../../../components/dashboard/layout-button.scss'], + standalone: false }) export class ManageDashboardLayoutsDialogComponent extends DialogComponent implements ErrorStateMatcher, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.ts index 2b9a430e3b..d377983169 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.ts @@ -28,10 +28,11 @@ export interface MoveWidgetsDialogResult { } @Component({ - selector: 'tb-move-widgets-dialog', - templateUrl: './move-widgets-dialog.component.html', - providers: [], - styleUrls: [] + selector: 'tb-move-widgets-dialog', + templateUrl: './move-widgets-dialog.component.html', + providers: [], + styleUrls: [], + standalone: false }) export class MoveWidgetsDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts index c9ad66d233..25e2a23a5d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts @@ -21,9 +21,10 @@ import { BreakpointId } from '@shared/models/dashboard.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; @Component({ - selector: 'tb-select-dashboard-breakpoint', - templateUrl: './select-dashboard-breakpoint.component.html', - styleUrls: ['./select-dashboard-breakpoint.component.scss'] + selector: 'tb-select-dashboard-breakpoint', + templateUrl: './select-dashboard-breakpoint.component.html', + styleUrls: ['./select-dashboard-breakpoint.component.scss'], + standalone: false }) export class SelectDashboardBreakpointComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.ts index 64f9153546..b90164e6c0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.ts @@ -43,10 +43,11 @@ export interface DashboardStateDialogData { } @Component({ - selector: 'tb-dashboard-state-dialog', - templateUrl: './dashboard-state-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: DashboardStateDialogComponent}], - styleUrls: [] + selector: 'tb-dashboard-state-dialog', + templateUrl: './dashboard-state-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: DashboardStateDialogComponent }], + styleUrls: [], + standalone: false }) export class DashboardStateDialogComponent extends DialogComponent diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts index 92be4229b6..c0b3668e2e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts @@ -29,9 +29,10 @@ import { EntityService } from '@core/http/entity.service'; import { MobileService } from '@core/services/mobile.service'; @Component({ - selector: 'tb-default-state-controller', - templateUrl: './default-state-controller.component.html', - styleUrls: ['./default-state-controller.component.scss'] + selector: 'tb-default-state-controller', + templateUrl: './default-state-controller.component.html', + styleUrls: ['./default-state-controller.component.scss'], + standalone: false }) export class DefaultStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts index 15e18573f2..f8dfff6c97 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts @@ -31,9 +31,10 @@ import { map, tap } from 'rxjs/operators'; import { MobileService } from '@core/services/mobile.service'; @Component({ - selector: 'tb-entity-state-controller', - templateUrl: './entity-state-controller.component.html', - styleUrls: ['./entity-state-controller.component.scss'] + selector: 'tb-entity-state-controller', + templateUrl: './entity-state-controller.component.html', + styleUrls: ['./entity-state-controller.component.scss'], + standalone: false }) export class EntityStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts index c570783684..7e0971aa4b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts @@ -53,9 +53,10 @@ export interface ManageDashboardStatesDialogResult { } @Component({ - selector: 'tb-manage-dashboard-states-dialog', - templateUrl: './manage-dashboard-states-dialog.component.html', - styleUrls: ['./manage-dashboard-states-dialog.component.scss'] + selector: 'tb-manage-dashboard-states-dialog', + templateUrl: './manage-dashboard-states-dialog.component.html', + styleUrls: ['./manage-dashboard-states-dialog.component.scss'], + standalone: false }) export class ManageDashboardStatesDialogComponent extends DialogComponent diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts index 3588b22158..2229e6c24b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts @@ -31,8 +31,9 @@ import { IStateControllerComponent } from '@home/components/dashboard-page/state import { BehaviorSubject, Subject } from 'rxjs'; @Directive({ - // eslint-disable-next-line @angular-eslint/directive-selector - selector: 'tb-states-component' + // eslint-disable-next-line @angular-eslint/directive-selector + selector: 'tb-states-component', + standalone: false }) export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.ts index 4aec56771c..cb43dce650 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.ts @@ -30,9 +30,10 @@ export interface DisplayWidgetTypesPanelData { } @Component({ - selector: 'tb-widget-types-panel', - templateUrl: './widget-types-panel.component.html', - styleUrls: ['./widget-types-panel.component.scss'] + selector: 'tb-widget-types-panel', + templateUrl: './widget-types-panel.component.html', + styleUrls: ['./widget-types-panel.component.scss'], + standalone: false }) export class DisplayWidgetTypesPanelComponent { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-view/dashboard-view.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-view/dashboard-view.component.ts index 3e22d0719e..14bbd20688 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-view/dashboard-view.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-view/dashboard-view.component.ts @@ -22,9 +22,10 @@ import { Dashboard } from '@shared/models/dashboard.models'; import { ActivatedRoute } from '@angular/router'; @Component({ - selector: 'tb-dashboard-view', - templateUrl: './dashboard-view.component.html', - styleUrls: ['./dashboard-view.component.scss'] + selector: 'tb-dashboard-view', + templateUrl: './dashboard-view.component.html', + styleUrls: ['./dashboard-view.component.scss'], + standalone: false }) export class DashboardViewComponent extends PageComponent { diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index 802b305d8e..43bed083cc 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -63,10 +63,11 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; @Component({ - selector: 'tb-dashboard', - templateUrl: './dashboard.component.html', - styleUrls: ['./dashboard.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, OnDestroy, AfterViewInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.ts index 0ef839365f..a8e4e1c88e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.ts @@ -23,9 +23,10 @@ import { DialogComponent } from '@app/shared/components/dialog.component'; import { DashboardLayoutId } from '@app/shared/models/dashboard.models'; @Component({ - selector: 'tb-select-target-layout-dialog', - templateUrl: './select-target-layout-dialog.component.html', - styleUrls: ['./layout-button.scss'] + selector: 'tb-select-target-layout-dialog', + templateUrl: './select-target-layout-dialog.component.html', + styleUrls: ['./layout-button.scss'], + standalone: false }) export class SelectTargetLayoutDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.ts index 7c10619b08..20f81fe724 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.ts @@ -30,10 +30,11 @@ export interface SelectTargetStateDialogData { } @Component({ - selector: 'tb-select-target-state-dialog', - templateUrl: './select-target-state-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: SelectTargetStateDialogComponent}], - styleUrls: [] + selector: 'tb-select-target-state-dialog', + templateUrl: './select-target-state-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: SelectTargetStateDialogComponent }], + styleUrls: [], + standalone: false }) export class SelectTargetStateDialogComponent extends DialogComponent diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.ts b/ui-ngx/src/app/modules/home/components/details-panel.component.ts index dd82bf9879..1ab62c3081 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.ts @@ -31,9 +31,10 @@ import { Subscription } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-details-panel', - templateUrl: './details-panel.component.html', - styleUrls: ['./details-panel.component.scss'] + selector: 'tb-details-panel', + templateUrl: './details-panel.component.html', + styleUrls: ['./details-panel.component.scss'], + standalone: false }) export class DetailsPanelComponent extends PageComponent implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/device/copy-device-credentials.component.ts b/ui-ngx/src/app/modules/home/components/device/copy-device-credentials.component.ts index 038c83b643..4d668d3255 100644 --- a/ui-ngx/src/app/modules/home/components/device/copy-device-credentials.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/copy-device-credentials.component.ts @@ -28,9 +28,10 @@ import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; @Component({ - selector: 'tb-copy-device-credentials', - templateUrl: './copy-device-credentials.component.html', - styleUrls: [] + selector: 'tb-copy-device-credentials', + templateUrl: './copy-device-credentials.component.html', + styleUrls: [], + standalone: false }) export class CopyDeviceCredentialsComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts index 7d7a3c2c6b..810993d6f5 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts @@ -36,21 +36,22 @@ import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; @Component({ - selector: 'tb-device-credentials-lwm2m-server', - templateUrl: './device-credentials-lwm2m-server.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceCredentialsLwm2mServerComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceCredentialsLwm2mServerComponent), - multi: true - } - ] + selector: 'tb-device-credentials-lwm2m-server', + templateUrl: './device-credentials-lwm2m-server.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceCredentialsLwm2mServerComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceCredentialsLwm2mServerComponent), + multi: true + } + ], + standalone: false }) export class DeviceCredentialsLwm2mServerComponent implements OnDestroy, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts index 5c602f1189..3800f2deb8 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts @@ -43,21 +43,22 @@ import { Store } from "@ngrx/store"; import { AppState } from "@core/core.state"; @Component({ - selector: 'tb-device-credentials-lwm2m', - templateUrl: './device-credentials-lwm2m.component.html', - styleUrls: ['./device-credentials-lwm2m.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceCredentialsLwm2mComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceCredentialsLwm2mComponent), - multi: true - } - ] + selector: 'tb-device-credentials-lwm2m', + templateUrl: './device-credentials-lwm2m.component.html', + styleUrls: ['./device-credentials-lwm2m.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceCredentialsLwm2mComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceCredentialsLwm2mComponent), + multi: true + } + ], + standalone: false }) export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts index 8c55b3bd6b..5ad1a1ac21 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts @@ -32,20 +32,22 @@ import { takeUntil } from 'rxjs/operators'; import { generateSecret, isDefinedAndNotNull, isEmptyStr } from '@core/utils'; @Component({ - selector: 'tb-device-credentials-mqtt-basic', - templateUrl: './device-credentials-mqtt-basic.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceCredentialsMqttBasicComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceCredentialsMqttBasicComponent), - multi: true, - }], - styleUrls: [] + selector: 'tb-device-credentials-mqtt-basic', + templateUrl: './device-credentials-mqtt-basic.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceCredentialsMqttBasicComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceCredentialsMqttBasicComponent), + multi: true, + } + ], + styleUrls: [], + standalone: false }) export class DeviceCredentialsMqttBasicComponent implements ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts index 11e82fafdf..1d1838a9ea 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts @@ -39,20 +39,22 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { DeviceId } from "@shared/models/id/device-id"; @Component({ - selector: 'tb-device-credentials', - templateUrl: './device-credentials.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceCredentialsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceCredentialsComponent), - multi: true, - }], - styleUrls: ['./device-credentials.component.scss'] + selector: 'tb-device-credentials', + templateUrl: './device-credentials.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceCredentialsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceCredentialsComponent), + multi: true, + } + ], + styleUrls: ['./device-credentials.component.scss'], + standalone: false }) export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy { 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 4e35be3060..48f873ef61 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 @@ -51,16 +51,17 @@ export interface DeviceFilterConfigData { // @dynamic @Component({ - selector: 'tb-device-info-filter', - templateUrl: './device-info-filter.component.html', - styleUrls: ['./device-info-filter.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceInfoFilterComponent), - multi: true - } - ] + selector: 'tb-device-info-filter', + templateUrl: './device-info-filter.component.html', + styleUrls: ['./device-info-filter.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceInfoFilterComponent), + multi: true + } + ], + standalone: false }) export class DeviceInfoFilterComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.ts b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.ts index d6e9c29c37..e1366d474c 100644 --- a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.ts @@ -22,9 +22,10 @@ import { EdgeEvent } from '@shared/models/edge.models'; import { EdgeDownlinkTableConfig } from '@home/components/edge/edge-downlink-table-config'; @Component({ - selector: 'tb-edge-downlink-table-header', - templateUrl: './edge-downlink-table-header.component.html', - styleUrls: ['./edge-downlink-table-header.component.scss'] + selector: 'tb-edge-downlink-table-header', + templateUrl: './edge-downlink-table-header.component.html', + styleUrls: ['./edge-downlink-table-header.component.scss'], + standalone: false }) export class EdgeDownlinkTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.ts b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.ts index 279381b110..8bb83eda19 100644 --- a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.ts @@ -29,9 +29,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-edge-downlink-table', - templateUrl: './edge-downlink-table.component.html', - styleUrls: ['./edge-downlink-table.component.scss'] + selector: 'tb-edge-downlink-table', + templateUrl: './edge-downlink-table.component.html', + styleUrls: ['./edge-downlink-table.component.scss'], + standalone: false }) export class EdgeDownlinkTableComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts index f117fd2a44..5894921d1a 100644 --- a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts @@ -31,10 +31,11 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; @Component({ - selector: 'tb-add-entity-dialog', - templateUrl: './add-entity-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AddEntityDialogComponent}], - styleUrls: ['./add-entity-dialog.component.scss'] + selector: 'tb-add-entity-dialog', + templateUrl: './add-entity-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AddEntityDialogComponent }], + styleUrls: ['./add-entity-dialog.component.scss'], + standalone: false }) export class AddEntityDialogComponent extends DialogComponent> implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts index f849f65d04..5d0ac5d5e1 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts @@ -39,23 +39,22 @@ import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entit import { EntityType } from '@shared/models/entity-type.models'; @Component({ - selector: 'tb-entity-debug-settings-button', - templateUrl: './entity-debug-settings-button.component.html', - standalone: true, - imports: [ - CommonModule, - SharedModule, - DurationLeftPipe, - ], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityDebugSettingsButtonComponent), - multi: true - }, - EntityDebugSettingsService - ], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-entity-debug-settings-button', + templateUrl: './entity-debug-settings-button.component.html', + imports: [ + CommonModule, + SharedModule, + DurationLeftPipe, + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityDebugSettingsButtonComponent), + multi: true + }, + EntityDebugSettingsService + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class EntityDebugSettingsButtonComponent implements ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts index 88dd185141..9b2fd42210 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts @@ -39,15 +39,14 @@ import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.m import { getCurrentAuthState } from '@core/auth/auth.selectors'; @Component({ - selector: 'tb-entity-debug-settings-panel', - templateUrl: './entity-debug-settings-panel.component.html', - standalone: true, - imports: [ - SharedModule, - CommonModule, - DurationLeftPipe - ], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-entity-debug-settings-panel', + templateUrl: './entity-debug-settings-panel.component.html', + imports: [ + SharedModule, + CommonModule, + DurationLeftPipe + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class EntityDebugSettingsPanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index ec04a7eed8..6999c76180 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -71,10 +71,11 @@ import { EntityDetailsPanelComponent } from '@home/components/entity/entity-deta import { FormBuilder } from '@angular/forms'; @Component({ - selector: 'tb-entities-table', - templateUrl: './entities-table.component.html', - styleUrls: ['./entities-table.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-entities-table', + templateUrl: './entities-table.component.html', + styleUrls: ['./entities-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class EntitiesTableComponent extends PageComponent implements IEntitiesTableComponent, AfterViewInit, OnInit, OnChanges, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts index eed41e2820..7dbe3d2949 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts @@ -21,9 +21,10 @@ import { baseDetailsPageByEntityType, EntityType } from '@app/shared/public-api' import { isEqual, isNotEmptyStr, isObject } from '@core/utils'; @Component({ - selector: 'tb-entity-chips', - templateUrl: './entity-chips.component.html', - styleUrls: ['./entity-chips.component.scss'] + selector: 'tb-entity-chips', + templateUrl: './entity-chips.component.html', + styleUrls: ['./entity-chips.component.scss'], + standalone: false }) export class EntityChipsComponent implements OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts index a66e04f23f..8988a02380 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts @@ -37,10 +37,11 @@ import { DialogService } from '@core/services/dialog.service'; import { IEntityDetailsPageComponent } from '@home/models/entity/entity-details-page-component.models'; @Component({ - selector: 'tb-entity-details-page', - templateUrl: './entity-details-page.component.html', - styleUrls: ['./entity-details-page.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-entity-details-page', + templateUrl: './entity-details-page.component.html', + styleUrls: ['./entity-details-page.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class EntityDetailsPageComponent extends EntityDetailsPanelComponent implements IEntityDetailsPageComponent, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index 88b981d9db..8c6fc004d8 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -47,10 +47,11 @@ import { catchError } from 'rxjs/operators'; import { HttpStatusCode } from '@angular/common/http'; @Component({ - selector: 'tb-entity-details-panel', - templateUrl: './entity-details-panel.component.html', - styleUrls: ['./entity-details-panel.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-entity-details-panel', + templateUrl: './entity-details-panel.component.html', + styleUrls: ['./entity-details-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class EntityDetailsPanelComponent extends PageComponent implements AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts index 7fb4768be4..ce5d78804d 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts @@ -21,16 +21,17 @@ import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/mod import { TranslateService } from '@ngx-translate/core'; @Component({ - selector: 'tb-entity-filter-view', - templateUrl: './entity-filter-view.component.html', - styleUrls: ['./entity-filter-view.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityFilterViewComponent), - multi: true - } - ] + selector: 'tb-entity-filter-view', + templateUrl: './entity-filter-view.component.html', + styleUrls: ['./entity-filter-view.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityFilterViewComponent), + multi: true + } + ], + standalone: false }) export class EntityFilterViewComponent implements ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts index 88c0035902..dcd127c4a3 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts @@ -24,16 +24,17 @@ import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-entity-filter', - templateUrl: './entity-filter.component.html', - styleUrls: ['./entity-filter.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityFilterComponent), - multi: true - } - ] + selector: 'tb-entity-filter', + templateUrl: './entity-filter.component.html', + styleUrls: ['./entity-filter.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityFilterComponent), + multi: true + } + ], + standalone: false }) export class EntityFilterComponent implements ControlValueAccessor, OnInit, OnDestroy { 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 90192e7fa0..7fb51c13f6 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 @@ -36,9 +36,10 @@ export interface EventContentDialogData { } @Component({ - selector: 'tb-event-content-dialog', - templateUrl: './event-content-dialog.component.html', - styleUrls: ['./event-content-dialog.component.scss'] + selector: 'tb-event-content-dialog', + templateUrl: './event-content-dialog.component.html', + styleUrls: ['./event-content-dialog.component.scss'], + standalone: false }) export class EventContentDialogComponent extends DialogComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts index efc24adb8d..6b50c95d76 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts @@ -35,9 +35,10 @@ export interface FilterEntityColumn { @Component({ - selector: 'tb-event-filter-panel', - templateUrl: './event-filter-panel.component.html', - styleUrls: ['./event-filter-panel.component.scss'] + selector: 'tb-event-filter-panel', + templateUrl: './event-filter-panel.component.html', + styleUrls: ['./event-filter-panel.component.scss'], + standalone: false }) export class EventFilterPanelComponent { diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-header.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.ts index d4cd493e73..867df4d6d2 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.ts @@ -22,9 +22,10 @@ import { DebugEventType, Event, EventType, eventTypeTranslations } from '@app/sh import { EventTableConfig } from '@home/components/event/event-table-config'; @Component({ - selector: 'tb-event-table-header', - templateUrl: './event-table-header.component.html', - styleUrls: ['./event-table-header.component.scss'] + selector: 'tb-event-table-header', + templateUrl: './event-table-header.component.html', + styleUrls: ['./event-table-header.component.scss'], + standalone: false }) export class EventTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts index 13b888d26b..df35d0b873 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts @@ -42,9 +42,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-event-table', - templateUrl: './event-table.component.html', - styleUrls: ['./event-table.component.scss'] + selector: 'tb-event-table', + templateUrl: './event-table.component.html', + styleUrls: ['./event-table.component.scss'], + standalone: false }) export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index 3f152e79fa..c3a445d4a3 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -35,21 +35,22 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-boolean-filter-predicate', - templateUrl: './boolean-filter-predicate.component.html', - styleUrls: ['./filter-predicate.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => BooleanFilterPredicateComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => BooleanFilterPredicateComponent), - multi: true - } - ] + selector: 'tb-boolean-filter-predicate', + templateUrl: './boolean-filter-predicate.component.html', + styleUrls: ['./filter-predicate.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BooleanFilterPredicateComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BooleanFilterPredicateComponent), + multi: true + } + ], + standalone: false }) export class BooleanFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts index f2770affb5..ceb5b63ce7 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts @@ -31,10 +31,11 @@ import { import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models'; @Component({ - selector: 'tb-complex-filter-predicate-dialog', - templateUrl: './complex-filter-predicate-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: ComplexFilterPredicateDialogComponent}], - styleUrls: [] + selector: 'tb-complex-filter-predicate-dialog', + templateUrl: './complex-filter-predicate-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: ComplexFilterPredicateDialogComponent }], + styleUrls: [], + standalone: false }) export class ComplexFilterPredicateDialogComponent extends DialogComponent diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts index 0df6a9d69b..88a2971b87 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts @@ -24,16 +24,17 @@ import { COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN } from '@home/component import { ComponentType } from '@angular/cdk/portal'; @Component({ - selector: 'tb-complex-filter-predicate', - templateUrl: './complex-filter-predicate.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ComplexFilterPredicateComponent), - multi: true - } - ] + selector: 'tb-complex-filter-predicate', + templateUrl: './complex-filter-predicate.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ComplexFilterPredicateComponent), + multi: true + } + ], + standalone: false }) export class ComplexFilterPredicateComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts index 75b56e15a5..ec3c9b79f0 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts @@ -41,10 +41,11 @@ export interface FilterDialogData { } @Component({ - selector: 'tb-filter-dialog', - templateUrl: './filter-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: FilterDialogComponent}], - styleUrls: ['./filter-dialog.component.scss'] + selector: 'tb-filter-dialog', + templateUrl: './filter-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: FilterDialogComponent }], + styleUrls: ['./filter-dialog.component.scss'], + standalone: false }) export class FilterDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts index 1fb3a00360..8ab2bd6e35 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts @@ -43,21 +43,22 @@ import { COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN } from '@home/component import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models'; @Component({ - selector: 'tb-filter-predicate-list', - templateUrl: './filter-predicate-list.component.html', - styleUrls: ['./filter-predicate-list.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FilterPredicateListComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => FilterPredicateListComponent), - multi: true - } - ] + selector: 'tb-filter-predicate-list', + templateUrl: './filter-predicate-list.component.html', + styleUrls: ['./filter-predicate-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterPredicateListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FilterPredicateListComponent), + multi: true + } + ], + standalone: false }) export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts index 01154c8930..14c334b72c 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts @@ -38,21 +38,22 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-filter-predicate-value', - templateUrl: './filter-predicate-value.component.html', - styleUrls: ['./filter-predicate.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FilterPredicateValueComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => FilterPredicateValueComponent), - multi: true - } - ] + selector: 'tb-filter-predicate-value', + templateUrl: './filter-predicate-value.component.html', + styleUrls: ['./filter-predicate.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterPredicateValueComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FilterPredicateValueComponent), + multi: true + } + ], + standalone: false }) export class FilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts index 170945e4ce..e4e327bf0a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts @@ -29,21 +29,22 @@ import { EntityKeyValueType, FilterPredicateType, KeyFilterPredicateInfo } from import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-filter-predicate', - templateUrl: './filter-predicate.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FilterPredicateComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => FilterPredicateComponent), - multi: true - } - ] + selector: 'tb-filter-predicate', + templateUrl: './filter-predicate.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterPredicateComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FilterPredicateComponent), + multi: true + } + ], + standalone: false }) export class FilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts index 2dce9164db..d2d53c0918 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts @@ -23,16 +23,17 @@ import { DatePipe } from '@angular/common'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; @Component({ - selector: 'tb-filter-text', - templateUrl: './filter-text.component.html', - styleUrls: ['./filter-text.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FilterTextComponent), - multi: true - } - ] + selector: 'tb-filter-text', + templateUrl: './filter-text.component.html', + styleUrls: ['./filter-text.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterTextComponent), + multi: true + } + ], + standalone: false }) export class FilterTextComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts index 0c4509f133..6eb7da12b2 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts @@ -43,10 +43,11 @@ export interface FilterUserInfoDialogData { } @Component({ - selector: 'tb-filter-user-info-dialog', - templateUrl: './filter-user-info-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: FilterUserInfoDialogComponent}], - styleUrls: [] + selector: 'tb-filter-user-info-dialog', + templateUrl: './filter-user-info-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: FilterUserInfoDialogComponent }], + styleUrls: [], + standalone: false }) export class FilterUserInfoDialogComponent extends DialogComponent diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts index 006bbf81f6..952e0ac9d6 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts @@ -30,16 +30,17 @@ import { import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-filter-user-info', - templateUrl: './filter-user-info.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FilterUserInfoComponent), - multi: true - } - ] + selector: 'tb-filter-user-info', + templateUrl: './filter-user-info.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterUserInfoComponent), + multi: true + } + ], + standalone: false }) export class FilterUserInfoComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts index 545c97bffd..8282c2cd21 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts @@ -52,10 +52,11 @@ export interface FiltersDialogData { } @Component({ - selector: 'tb-filters-dialog', - templateUrl: './filters-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: FiltersDialogComponent}], - styleUrls: ['./filters-dialog.component.scss'] + selector: 'tb-filters-dialog', + templateUrl: './filters-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: FiltersDialogComponent }], + styleUrls: ['./filters-dialog.component.scss'], + standalone: false }) export class FiltersDialogComponent extends DialogComponent implements ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts index ccb05c7f0c..06a218394c 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts @@ -29,9 +29,10 @@ export interface FiltersEditPanelData { } @Component({ - selector: 'tb-filters-edit-panel', - templateUrl: './filters-edit-panel.component.html', - styleUrls: ['./filters-edit-panel.component.scss'] + selector: 'tb-filters-edit-panel', + templateUrl: './filters-edit-panel.component.html', + styleUrls: ['./filters-edit-panel.component.scss'], + standalone: false }) export class FiltersEditPanelComponent { diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts index 48a33a8765..5bbd4ac034 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts @@ -42,9 +42,10 @@ import { UserFilterDialogComponent, UserFilterDialogData } from '@home/component import { MatDialog } from '@angular/material/dialog'; @Component({ - selector: 'tb-filters-edit', - templateUrl: './filters-edit.component.html', - styleUrls: ['./filters-edit.component.scss'] + selector: 'tb-filters-edit', + templateUrl: './filters-edit.component.html', + styleUrls: ['./filters-edit.component.scss'], + standalone: false }) export class FiltersEditComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index e6e58c33cb..ffaa10570d 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -50,10 +50,11 @@ export interface KeyFilterDialogData { } @Component({ - selector: 'tb-key-filter-dialog', - templateUrl: './key-filter-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent}], - styleUrls: ['./key-filter-dialog.component.scss'] + selector: 'tb-key-filter-dialog', + templateUrl: './key-filter-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent }], + styleUrls: ['./key-filter-dialog.component.scss'], + standalone: false }) export class KeyFilterDialogComponent extends DialogComponent diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts index ec3075f9f8..aad686707d 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts @@ -42,21 +42,22 @@ import { EntityId } from '@shared/models/id/entity-id'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-key-filter-list', - templateUrl: './key-filter-list.component.html', - styleUrls: ['./key-filter-list.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => KeyFilterListComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => KeyFilterListComponent), - multi: true - } - ] + selector: 'tb-key-filter-list', + templateUrl: './key-filter-list.component.html', + styleUrls: ['./key-filter-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KeyFilterListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KeyFilterListComponent), + multi: true + } + ], + standalone: false }) export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts index 6d5952c1ee..3fb257f55f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts @@ -35,21 +35,22 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-numeric-filter-predicate', - templateUrl: './numeric-filter-predicate.component.html', - styleUrls: ['./filter-predicate.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => NumericFilterPredicateComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => NumericFilterPredicateComponent), - multi: true - } - ] + selector: 'tb-numeric-filter-predicate', + templateUrl: './numeric-filter-predicate.component.html', + styleUrls: ['./filter-predicate.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NumericFilterPredicateComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NumericFilterPredicateComponent), + multi: true + } + ], + standalone: false }) export class NumericFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts index c2fac83db0..d4e3be6b5f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts @@ -35,21 +35,22 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-string-filter-predicate', - templateUrl: './string-filter-predicate.component.html', - styleUrls: ['./filter-predicate.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => StringFilterPredicateComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => StringFilterPredicateComponent), - multi: true - } - ] + selector: 'tb-string-filter-predicate', + templateUrl: './string-filter-predicate.component.html', + styleUrls: ['./filter-predicate.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => StringFilterPredicateComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => StringFilterPredicateComponent), + multi: true + } + ], + standalone: false }) export class StringFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts index 279b257178..6e068fdcf3 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts @@ -40,10 +40,11 @@ export interface UserFilterDialogData { } @Component({ - selector: 'tb-user-filter-dialog', - templateUrl: './user-filter-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent}], - styleUrls: ['./user-filter-dialog.component.scss'], + selector: 'tb-user-filter-dialog', + templateUrl: './user-filter-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent }], + styleUrls: ['./user-filter-dialog.component.scss'], + standalone: false }) export class UserFilterDialogComponent extends DialogComponent implements ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.ts b/ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.ts index a1ec8318d3..7b12265e66 100644 --- a/ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.ts +++ b/ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.ts @@ -27,9 +27,10 @@ import { Subject } from 'rxjs'; const SETTINGS_KEY = 'HIDE_GITHUB_STAR_BUTTON'; @Component({ - selector: 'tb-github-badge', - templateUrl: './github-badge.component.html', - styleUrl: './github-badge.component.scss' + selector: 'tb-github-badge', + templateUrl: './github-badge.component.html', + styleUrl: './github-badge.component.scss', + standalone: false }) export class GithubBadgeComponent implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts b/ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts index 4060e4c54f..40df0ed280 100644 --- a/ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts +++ b/ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts @@ -35,9 +35,10 @@ import { selectIsAuthenticated } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-notification-bell', - templateUrl: './notification-bell.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-notification-bell', + templateUrl: './notification-bell.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class NotificationBellComponent implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.ts b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.ts index 8bb603d023..d159baecd4 100644 --- a/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.ts @@ -30,8 +30,9 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; @Component({ - selector: 'tb-send-notification-button', - templateUrl: './send-notification-button.component.html', + selector: 'tb-send-notification-button', + templateUrl: './send-notification-button.component.html', + standalone: false }) export class SendNotificationButtonComponent { diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts index 65891d77c3..f9e4969788 100644 --- a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts +++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts @@ -28,9 +28,10 @@ import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.model import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-show-notification-popover', - templateUrl: './show-notification-popover.component.html', - styleUrls: ['show-notification-popover.component.scss'] + selector: 'tb-show-notification-popover', + templateUrl: './show-notification-popover.component.html', + styleUrls: ['show-notification-popover.component.scss'], + standalone: false }) export class ShowNotificationPopoverComponent extends PageComponent implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts index 05b8517e55..367aced7c1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -54,10 +54,11 @@ export interface AddDeviceProfileDialogData { } @Component({ - selector: 'tb-add-device-profile-dialog', - templateUrl: './add-device-profile-dialog.component.html', - providers: [], - styleUrls: ['./add-device-profile-dialog.component.scss'] + selector: 'tb-add-device-profile-dialog', + templateUrl: './add-device-profile-dialog.component.html', + providers: [], + styleUrls: ['./add-device-profile-dialog.component.scss'], + standalone: false }) export class AddDeviceProfileDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.ts index d0a08dce25..fe8945fd83 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.ts @@ -27,16 +27,17 @@ import { AlarmConditionType } from '@shared/models/device.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-alarm-duration-predicate-value', - templateUrl: './alarm-duration-predicate-value.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmDurationPredicateValueComponent), - multi: true - } - ] + selector: 'tb-alarm-duration-predicate-value', + templateUrl: './alarm-duration-predicate-value.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmDurationPredicateValueComponent), + multi: true + } + ], + standalone: false }) export class AlarmDurationPredicateValueComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.ts index af7e4b07be..da5022571a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.ts @@ -29,13 +29,14 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-alarm-dynamic-value', - templateUrl: './alarm-dynamic-value.component.html', - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmDynamicValue), - multi: true - }] + selector: 'tb-alarm-dynamic-value', + templateUrl: './alarm-dynamic-value.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmDynamicValue), + multi: true + }], + standalone: false }) export class AlarmDynamicValue implements ControlValueAccessor, OnInit{ diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts index 0f3f183341..ba16a910db 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts @@ -36,10 +36,11 @@ export interface AlarmRuleConditionDialogData { } @Component({ - selector: 'tb-alarm-rule-condition-dialog', - templateUrl: './alarm-rule-condition-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent}], - styleUrls: ['./alarm-rule-condition-dialog.component.scss'] + selector: 'tb-alarm-rule-condition-dialog', + templateUrl: './alarm-rule-condition-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent }], + styleUrls: ['./alarm-rule-condition-dialog.component.scss'], + standalone: false }) export class AlarmRuleConditionDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts index df98b4906a..944e73b508 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts @@ -38,21 +38,22 @@ import { EntityId } from '@shared/models/id/entity-id'; import { dynamicValueSourceTypeTranslationMap } from '@shared/models/query/query.models'; @Component({ - selector: 'tb-alarm-rule-condition', - templateUrl: './alarm-rule-condition.component.html', - styleUrls: ['./alarm-rule-condition.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmRuleConditionComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AlarmRuleConditionComponent), - multi: true, - } - ] + selector: 'tb-alarm-rule-condition', + templateUrl: './alarm-rule-condition.component.html', + styleUrls: ['./alarm-rule-condition.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleConditionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleConditionComponent), + multi: true, + } + ], + standalone: false }) export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts index a3238e4385..a6506aa33d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -39,21 +39,22 @@ import { UtilsService } from '@core/services/utils.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-alarm-rule', - templateUrl: './alarm-rule.component.html', - styleUrls: ['./alarm-rule.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmRuleComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AlarmRuleComponent), - multi: true, - } - ] + selector: 'tb-alarm-rule', + templateUrl: './alarm-rule.component.html', + styleUrls: ['./alarm-rule.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleComponent), + multi: true, + } + ], + standalone: false }) export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts index 24424f5048..f9224cdccc 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts @@ -32,10 +32,11 @@ export interface AlarmScheduleDialogData { } @Component({ - selector: 'tb-alarm-schedule-dialog', - templateUrl: './alarm-schedule-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AlarmScheduleDialogComponent}], - styleUrls: [] + selector: 'tb-alarm-schedule-dialog', + templateUrl: './alarm-schedule-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AlarmScheduleDialogComponent }], + styleUrls: [], + standalone: false }) export class AlarmScheduleDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts index a0257da8ad..30209a2f7c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts @@ -31,14 +31,15 @@ import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; @Component({ - selector: 'tb-alarm-schedule-info', - templateUrl: './alarm-schedule-info.component.html', - styleUrls: ['./alarm-schedule-info.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmScheduleInfoComponent), - multi: true - }] + selector: 'tb-alarm-schedule-info', + templateUrl: './alarm-schedule-info.component.html', + styleUrls: ['./alarm-schedule-info.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmScheduleInfoComponent), + multi: true + }], + standalone: false }) export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts index 384a266a08..90caf04b70 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts @@ -43,18 +43,19 @@ import { getDefaultTimezone } from '@shared/models/time/time.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-alarm-schedule', - templateUrl: './alarm-schedule.component.html', - styleUrls: ['./alarm-schedule.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmScheduleComponent), - multi: true - }, { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AlarmScheduleComponent), - multi: true - }] + selector: 'tb-alarm-schedule', + templateUrl: './alarm-schedule.component.html', + styleUrls: ['./alarm-schedule.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmScheduleComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmScheduleComponent), + multi: true + }], + standalone: false }) export class AlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit { @Input() diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts index b81c65dbcf..4445f45ca8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts @@ -35,21 +35,22 @@ import { EntityId } from '@shared/models/id/entity-id'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-create-alarm-rules', - templateUrl: './create-alarm-rules.component.html', - styleUrls: ['./create-alarm-rules.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CreateAlarmRulesComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CreateAlarmRulesComponent), - multi: true, - } - ] + selector: 'tb-create-alarm-rules', + templateUrl: './create-alarm-rules.component.html', + styleUrls: ['./create-alarm-rules.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CreateAlarmRulesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CreateAlarmRulesComponent), + multi: true, + } + ], + standalone: false }) export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts index cf42c444d2..a282f1ee0b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts @@ -34,21 +34,22 @@ import { UtilsService } from '@core/services/utils.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device-profile-alarm', - templateUrl: './device-profile-alarm.component.html', - styleUrls: ['./device-profile-alarm.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceProfileAlarmComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceProfileAlarmComponent), - multi: true, - } - ] + selector: 'tb-device-profile-alarm', + templateUrl: './device-profile-alarm.component.html', + styleUrls: ['./device-profile-alarm.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAlarmComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileAlarmComponent), + multi: true, + } + ], + standalone: false }) export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts index ff0f824cf1..14e8d0d32d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts @@ -37,21 +37,22 @@ import { EntityId } from '@shared/models/id/entity-id'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-device-profile-alarms', - templateUrl: './device-profile-alarms.component.html', - styleUrls: ['./device-profile-alarms.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceProfileAlarmsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceProfileAlarmsComponent), - multi: true, - } - ] + selector: 'tb-device-profile-alarms', + templateUrl: './device-profile-alarms.component.html', + styleUrls: ['./device-profile-alarms.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAlarmsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileAlarmsComponent), + multi: true, + } + ], + standalone: false }) export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts index 4078930902..45f13a4487 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts @@ -31,10 +31,11 @@ export interface EditAlarmDetailsDialogData { } @Component({ - selector: 'tb-edit-alarm-details-dialog', - templateUrl: './edit-alarm-details-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: EditAlarmDetailsDialogComponent}], - styleUrls: [] + selector: 'tb-edit-alarm-details-dialog', + templateUrl: './edit-alarm-details-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: EditAlarmDetailsDialogComponent }], + styleUrls: [], + standalone: false }) export class EditAlarmDetailsDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts index 0c2a7bbd9f..79b4c6ed4d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts @@ -51,14 +51,15 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; @Component({ - selector: 'tb-asset-profile-autocomplete', - templateUrl: './asset-profile-autocomplete.component.html', - styleUrls: ['./asset-profile-autocomplete.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AssetProfileAutocompleteComponent), - multi: true - }] + selector: 'tb-asset-profile-autocomplete', + templateUrl: './asset-profile-autocomplete.component.html', + styleUrls: ['./asset-profile-autocomplete.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AssetProfileAutocompleteComponent), + multi: true + }], + standalone: false }) export class AssetProfileAutocompleteComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts index 1b196536f1..11ba3aabf4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts @@ -32,10 +32,11 @@ export interface AssetProfileDialogData { } @Component({ - selector: 'tb-asset-profile-dialog', - templateUrl: './asset-profile-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AssetProfileDialogComponent}], - styleUrls: [] + selector: 'tb-asset-profile-dialog', + templateUrl: './asset-profile-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AssetProfileDialogComponent }], + styleUrls: [], + standalone: false }) export class AssetProfileDialogComponent extends DialogComponent implements ErrorStateMatcher, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts index 7c78ea881f..18374dc7df 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts @@ -31,9 +31,10 @@ import { AssetProfile } from '@shared/models/asset.models'; import { RuleChainType } from '@shared/models/rule-chain.models'; @Component({ - selector: 'tb-asset-profile', - templateUrl: './asset-profile.component.html', - styleUrls: [] + selector: 'tb-asset-profile', + templateUrl: './asset-profile.component.html', + styleUrls: [], + standalone: false }) export class AssetProfileComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts index b26bbb7817..c041d0420a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts @@ -54,14 +54,15 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; @Component({ - selector: 'tb-device-profile-autocomplete', - templateUrl: './device-profile-autocomplete.component.html', - styleUrls: ['./device-profile-autocomplete.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceProfileAutocompleteComponent), - multi: true - }] + selector: 'tb-device-profile-autocomplete', + templateUrl: './device-profile-autocomplete.component.html', + styleUrls: ['./device-profile-autocomplete.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAutocompleteComponent), + multi: true + }], + standalone: false }) export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts index 2f791f6cda..eacc40e4d8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts @@ -32,10 +32,11 @@ export interface DeviceProfileDialogData { } @Component({ - selector: 'tb-device-profile-dialog', - templateUrl: './device-profile-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: DeviceProfileDialogComponent}], - styleUrls: [] + selector: 'tb-device-profile-dialog', + templateUrl: './device-profile-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: DeviceProfileDialogComponent }], + styleUrls: [], + standalone: false }) export class DeviceProfileDialogComponent extends DialogComponent implements ErrorStateMatcher, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 6ba2a62f0e..1da59bbc5c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -40,21 +40,22 @@ import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device-profile-provision-configuration', - templateUrl: './device-profile-provision-configuration.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceProfileProvisionConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceProfileProvisionConfigurationComponent), - multi: true, - } - ] + selector: 'tb-device-profile-provision-configuration', + templateUrl: './device-profile-provision-configuration.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileProvisionConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileProvisionConfigurationComponent), + multi: true, + } + ], + standalone: false }) export class DeviceProfileProvisionConfigurationComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts index c7533cd09f..1c7c52ec32 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -46,9 +46,10 @@ import { RuleChainType } from '@shared/models/rule-chain.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device-profile', - templateUrl: './device-profile.component.html', - styleUrls: [] + selector: 'tb-device-profile', + templateUrl: './device-profile.component.html', + styleUrls: [], + standalone: false }) export class DeviceProfileComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts index d264626399..72c1552978 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts @@ -46,21 +46,22 @@ import { takeUntil } from 'rxjs/operators'; import { PowerMode } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; @Component({ - selector: 'tb-coap-device-profile-transport-configuration', - templateUrl: './coap-device-profile-transport-configuration.component.html', - styleUrls: ['./coap-device-profile-transport-configuration.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CoapDeviceProfileTransportConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CoapDeviceProfileTransportConfigurationComponent), - multi: true - } - ] + selector: 'tb-coap-device-profile-transport-configuration', + templateUrl: './coap-device-profile-transport-configuration.component.html', + styleUrls: ['./coap-device-profile-transport-configuration.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CoapDeviceProfileTransportConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CoapDeviceProfileTransportConfigurationComponent), + multi: true + } + ], + standalone: false }) export class CoapDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.ts index 7b653f43e0..ec4576a162 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.ts @@ -26,9 +26,10 @@ import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; @Component({ - selector: 'tb-power-mode-settings', - templateUrl: './power-mode-setting.component.html', - styleUrls: [] + selector: 'tb-power-mode-settings', + templateUrl: './power-mode-setting.component.html', + styleUrls: [], + standalone: false }) export class PowerModeSettingComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts index 49f84c14fa..709d4ececa 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts @@ -44,21 +44,22 @@ interface FormGroupModel { } @Component({ - selector: 'tb-time-unit-select', - templateUrl: './time-unit-select.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeUnitSelectComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TimeUnitSelectComponent), - multi: true - } - ] + selector: 'tb-time-unit-select', + templateUrl: './time-unit-select.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeUnitSelectComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeUnitSelectComponent), + multi: true + } + ], + standalone: false }) export class TimeUnitSelectComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts index 0f082fd97a..e9c5ef36dc 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts @@ -26,14 +26,15 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-default-device-profile-configuration', - templateUrl: './default-device-profile-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DefaultDeviceProfileConfigurationComponent), - multi: true - }] + selector: 'tb-default-device-profile-configuration', + templateUrl: './default-device-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceProfileConfigurationComponent), + multi: true + }], + standalone: false }) export class DefaultDeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts index 1b7db15d7c..dcb8abc4a3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts @@ -32,21 +32,22 @@ import { DefaultDeviceProfileTransportConfiguration, DeviceTransportType } from import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-default-device-profile-transport-configuration', - templateUrl: './default-device-profile-transport-configuration.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), - multi: true - } - ] + selector: 'tb-default-device-profile-transport-configuration', + templateUrl: './default-device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), + multi: true + } + ], + standalone: false }) export class DefaultDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts index 80d2a8e0d7..5e6cc15409 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts @@ -24,14 +24,15 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-device-profile-configuration', - templateUrl: './device-profile-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceProfileConfigurationComponent), - multi: true - }] + selector: 'tb-device-profile-configuration', + templateUrl: './device-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileConfigurationComponent), + multi: true + }], + standalone: false }) export class DeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts index 8ef5804433..574e14ac7e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts @@ -32,21 +32,22 @@ import { deepClone } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device-profile-transport-configuration', - templateUrl: './device-profile-transport-configuration.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceProfileTransportConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceProfileTransportConfigurationComponent), - multi: true, - } - ] + selector: 'tb-device-profile-transport-configuration', + templateUrl: './device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileTransportConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileTransportConfigurationComponent), + multi: true, + } + ], + standalone: false }) export class DeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts index 595b62ffde..c61622963f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts @@ -32,10 +32,11 @@ export interface Lwm2mAttributesDialogData { } @Component({ - selector: 'tb-lwm2m-attributes-dialog', - templateUrl: './lwm2m-attributes-dialog.component.html', - styleUrls: [], - providers: [{provide: ErrorStateMatcher, useExisting: Lwm2mAttributesDialogComponent}], + selector: 'tb-lwm2m-attributes-dialog', + templateUrl: './lwm2m-attributes-dialog.component.html', + styleUrls: [], + providers: [{ provide: ErrorStateMatcher, useExisting: Lwm2mAttributesDialogComponent }], + standalone: false }) export class Lwm2mAttributesDialogComponent extends DialogComponent implements ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts index d17b37b5fa..0d6cf4df1d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts @@ -41,21 +41,22 @@ import { PageComponent } from '@shared/components/page.component'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-lwm2m-attributes-key-list', - templateUrl: './lwm2m-attributes-key-list.component.html', - styleUrls: ['./lwm2m-attributes-key-list.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mAttributesKeyListComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => Lwm2mAttributesKeyListComponent), - multi: true, - } - ] + selector: 'tb-lwm2m-attributes-key-list', + templateUrl: './lwm2m-attributes-key-list.component.html', + styleUrls: ['./lwm2m-attributes-key-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mAttributesKeyListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mAttributesKeyListComponent), + multi: true, + } + ], + standalone: false }) export class Lwm2mAttributesKeyListComponent extends PageComponent implements ControlValueAccessor, OnDestroy, OnDestroy, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts index af12a85389..a3a29e6961 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts @@ -25,14 +25,15 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-profile-lwm2m-attributes', - templateUrl: './lwm2m-attributes.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mAttributesComponent), - multi: true - }] + selector: 'tb-profile-lwm2m-attributes', + templateUrl: './lwm2m-attributes.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mAttributesComponent), + multi: true + }], + standalone: false }) export class Lwm2mAttributesComponent implements ControlValueAccessor, OnDestroy { attributesFormGroup: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.ts index d9ca2426a6..e50fe79cda 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.ts @@ -27,8 +27,9 @@ import { } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; @Component({ - selector: 'tb-profile-lwm2m-bootstrap-add-config-server-dialog', - templateUrl: './lwm2m-bootstrap-add-config-server-dialog.component.html' + selector: 'tb-profile-lwm2m-bootstrap-add-config-server-dialog', + templateUrl: './lwm2m-bootstrap-add-config-server-dialog.component.html', + standalone: false }) export class Lwm2mBootstrapAddConfigServerDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts index a64e262213..2f0fd69102 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts @@ -35,20 +35,21 @@ import { DeviceProfileService } from '@core/http/device-profile.service'; import { Lwm2mSecurityType } from '@shared/models/lwm2m-security-config.models'; @Component({ - selector: 'tb-profile-lwm2m-bootstrap-config-servers', - templateUrl: './lwm2m-bootstrap-config-servers.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mBootstrapConfigServersComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => Lwm2mBootstrapConfigServersComponent), - multi: true, - } - ] + selector: 'tb-profile-lwm2m-bootstrap-config-servers', + templateUrl: './lwm2m-bootstrap-config-servers.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mBootstrapConfigServersComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mBootstrapConfigServersComponent), + multi: true, + } + ], + standalone: false }) export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValueAccessor, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts index 823c1bb5b7..ba8a1c3fc1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts @@ -41,20 +41,21 @@ import { import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-profile-lwm2m-device-config-server', - templateUrl: './lwm2m-device-config-server.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mDeviceConfigServerComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => Lwm2mDeviceConfigServerComponent), - multi: true - }, - ] + selector: 'tb-profile-lwm2m-device-config-server', + templateUrl: './lwm2m-device-config-server.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mDeviceConfigServerComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mDeviceConfigServerComponent), + multi: true + }, + ], + standalone: false }) export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index 06c19c88c0..cf1083e3cc 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -58,20 +58,22 @@ import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; @Component({ - selector: 'tb-profile-lwm2m-device-transport-configuration', - templateUrl: './lwm2m-device-profile-transport-configuration.component.html', - styleUrls: ['./lwm2m-device-profile-transport-configuration.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mDeviceProfileTransportConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => Lwm2mDeviceProfileTransportConfigurationComponent), - multi: true - }] + selector: 'tb-profile-lwm2m-device-transport-configuration', + templateUrl: './lwm2m-device-profile-transport-configuration.component.html', + styleUrls: ['./lwm2m-device-profile-transport-configuration.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mDeviceProfileTransportConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mDeviceProfileTransportConfigurationComponent), + multi: true + } + ], + standalone: false }) export class Lwm2mDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.ts index 2a2db7551b..0b8c23bbe4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.ts @@ -29,8 +29,9 @@ export interface Lwm2mObjectAddInstancesData { } @Component({ - selector: 'tb-lwm2m-object-add-instances', - templateUrl: './lwm2m-object-add-instances-dialog.component.html' + selector: 'tb-lwm2m-object-add-instances', + templateUrl: './lwm2m-object-add-instances-dialog.component.html', + standalone: false }) export class Lwm2mObjectAddInstancesDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts index f139d40d2f..9c9d25e2f0 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts @@ -29,19 +29,21 @@ import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; @Component({ - selector: 'tb-profile-lwm2m-object-add-instances-list', - templateUrl: './lwm2m-object-add-instances-list.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mObjectAddInstancesListComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => Lwm2mObjectAddInstancesListComponent), - multi: true - }] + selector: 'tb-profile-lwm2m-object-add-instances-list', + templateUrl: './lwm2m-object-add-instances-list.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mObjectAddInstancesListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mObjectAddInstancesListComponent), + multi: true + } + ], + standalone: false }) export class Lwm2mObjectAddInstancesListComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts index 2db720da8c..3aafd2d56f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts @@ -37,20 +37,21 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; @Component({ - selector: 'tb-profile-lwm2m-object-list', - templateUrl: './lwm2m-object-list.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mObjectListComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => Lwm2mObjectListComponent), - multi: true - } - ] + selector: 'tb-profile-lwm2m-object-list', + templateUrl: './lwm2m-object-list.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mObjectListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mObjectListComponent), + multi: true + } + ], + standalone: false }) export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.ts index ecfeb9cf55..1bdd5cba82 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.ts @@ -34,21 +34,22 @@ import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; @Component({ - selector: 'tb-profile-lwm2m-observe-attr-telemetry-instances', - templateUrl: './lwm2m-observe-attr-telemetry-instances.component.html', - styleUrls: [ './lwm2m-observe-attr-telemetry-instances.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryInstancesComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryInstancesComponent), - multi: true - } - ] + selector: 'tb-profile-lwm2m-observe-attr-telemetry-instances', + templateUrl: './lwm2m-observe-attr-telemetry-instances.component.html', + styleUrls: ['./lwm2m-observe-attr-telemetry-instances.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryInstancesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryInstancesComponent), + multi: true + } + ], + standalone: false }) export class Lwm2mObserveAttrTelemetryInstancesComponent implements ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.ts index d3686b002f..0e107d43bf 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.ts @@ -33,21 +33,22 @@ import { combineLatest, Subject } from 'rxjs'; import { startWith, takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-profile-lwm2m-observe-attr-telemetry-resource', - templateUrl: './lwm2m-observe-attr-telemetry-resources.component.html', - styleUrls: ['./lwm2m-observe-attr-telemetry-resources.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryResourcesComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryResourcesComponent), - multi: true - } - ] + selector: 'tb-profile-lwm2m-observe-attr-telemetry-resource', + templateUrl: './lwm2m-observe-attr-telemetry-resources.component.html', + styleUrls: ['./lwm2m-observe-attr-telemetry-resources.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryResourcesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryResourcesComponent), + multi: true + } + ], + standalone: false }) export class Lwm2mObserveAttrTelemetryResourcesComponent implements ControlValueAccessor, OnDestroy, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts index 2de0b0f0a7..c2d5a21537 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts @@ -39,21 +39,22 @@ import _ from 'lodash'; import { Subscription } from 'rxjs'; @Component({ - selector: 'tb-profile-lwm2m-observe-attr-telemetry', - templateUrl: './lwm2m-observe-attr-telemetry.component.html', - styleUrls: [ './lwm2m-observe-attr-telemetry.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryComponent), - multi: true - } - ] + selector: 'tb-profile-lwm2m-observe-attr-telemetry', + templateUrl: './lwm2m-observe-attr-telemetry.component.html', + styleUrls: ['./lwm2m-observe-attr-telemetry.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryComponent), + multi: true + } + ], + standalone: false }) export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor, OnDestroy, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts index 9ade4171bf..bc997d6960 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -44,21 +44,22 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-mqtt-device-profile-transport-configuration', - templateUrl: './mqtt-device-profile-transport-configuration.component.html', - styleUrls: ['./mqtt-device-profile-transport-configuration.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent), - multi: true - } - ] + selector: 'tb-mqtt-device-profile-transport-configuration', + templateUrl: './mqtt-device-profile-transport-configuration.component.html', + styleUrls: ['./mqtt-device-profile-transport-configuration.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent), + multi: true + } + ], + standalone: false }) export class MqttDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts index 6811f636dd..544fcfdac6 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts @@ -32,20 +32,22 @@ import { isUndefinedOrNull } from '@core/utils'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-snmp-device-profile-communication-config', - templateUrl: './snmp-device-profile-communication-config.component.html', - styleUrls: ['./snmp-device-profile-communication-config.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent), - multi: true - }] + selector: 'tb-snmp-device-profile-communication-config', + templateUrl: './snmp-device-profile-communication-config.component.html', + styleUrls: ['./snmp-device-profile-communication-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent), + multi: true + } + ], + standalone: false }) export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.ts index b18a1c37f0..66f8c0737e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.ts @@ -34,20 +34,22 @@ import { isUndefinedOrNull } from '@core/utils'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-snmp-device-profile-mapping', - templateUrl: './snmp-device-profile-mapping.component.html', - styleUrls: ['./snmp-device-profile-mapping.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent), - multi: true - }] + selector: 'tb-snmp-device-profile-mapping', + templateUrl: './snmp-device-profile-mapping.component.html', + styleUrls: ['./snmp-device-profile-mapping.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent), + multi: true + } + ], + standalone: false }) export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts index e2681319c2..15338a385d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts @@ -42,20 +42,22 @@ export interface OidMappingConfiguration { } @Component({ - selector: 'tb-snmp-device-profile-transport-configuration', - templateUrl: './snmp-device-profile-transport-configuration.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent), - multi: true - }] + selector: 'tb-snmp-device-profile-transport-configuration', + templateUrl: './snmp-device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent), + multi: true + } + ], + standalone: false }) export class SnmpDeviceProfileTransportConfigurationComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts index c2166ae859..aa7bebaba8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts @@ -37,21 +37,22 @@ import { guid } from '@core/utils'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-tenant-profile-queues', - templateUrl: './tenant-profile-queues.component.html', - styleUrls: ['./tenant-profile-queues.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TenantProfileQueuesComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TenantProfileQueuesComponent), - multi: true, - } - ] + selector: 'tb-tenant-profile-queues', + templateUrl: './tenant-profile-queues.component.html', + styleUrls: ['./tenant-profile-queues.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileQueuesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TenantProfileQueuesComponent), + multi: true, + } + ], + standalone: false }) export class TenantProfileQueuesComponent implements ControlValueAccessor, Validator, OnDestroy, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts index 207eb19253..1bd785cf7a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts @@ -37,14 +37,15 @@ import { emptyPageData } from '@shared/models/page/page-data'; import { getEntityDetailsPageURL } from '@core/utils'; @Component({ - selector: 'tb-tenant-profile-autocomplete', - templateUrl: './tenant-profile-autocomplete.component.html', - styleUrls: ['./tenant-profile-autocomplete.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TenantProfileAutocompleteComponent), - multi: true - }] + selector: 'tb-tenant-profile-autocomplete', + templateUrl: './tenant-profile-autocomplete.component.html', + styleUrls: ['./tenant-profile-autocomplete.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileAutocompleteComponent), + multi: true + }], + standalone: false }) export class TenantProfileAutocompleteComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts index 543fdf9e1a..695fa2ceca 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts @@ -23,14 +23,15 @@ import { TenantProfileData } from '@shared/models/tenant.model'; import { Subscription } from 'rxjs'; @Component({ - selector: 'tb-tenant-profile-data', - templateUrl: './tenant-profile-data.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TenantProfileDataComponent), - multi: true - }] + selector: 'tb-tenant-profile-data', + templateUrl: './tenant-profile-data.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileDataComponent), + multi: true + }], + standalone: false }) export class TenantProfileDataComponent implements ControlValueAccessor, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts index 4f10376eaa..67ca86a215 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.ts @@ -32,10 +32,11 @@ export interface TenantProfileDialogData { } @Component({ - selector: 'tb-tenant-profile-dialog', - templateUrl: './tenant-profile-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: TenantProfileDialogComponent}], - styleUrls: ['tenant-profile-dialog.component.scss'] + selector: 'tb-tenant-profile-dialog', + templateUrl: './tenant-profile-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: TenantProfileDialogComponent }], + styleUrls: ['tenant-profile-dialog.component.scss'], + standalone: false }) export class TenantProfileDialogComponent extends DialogComponent implements ErrorStateMatcher, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts index 5025ce556a..4ae02c8ac8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts @@ -27,9 +27,10 @@ import { guid } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-tenant-profile', - templateUrl: './tenant-profile.component.html', - styleUrls: ['./tenant-profile.component.scss'] + selector: 'tb-tenant-profile', + templateUrl: './tenant-profile.component.html', + styleUrls: ['./tenant-profile.component.scss'], + standalone: false }) export class TenantProfileComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index be674b78fc..9fe6232f4c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -23,14 +23,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-default-tenant-profile-configuration', - templateUrl: './default-tenant-profile-configuration.component.html', - styleUrls: ['./default-tenant-profile-configuration.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DefaultTenantProfileConfigurationComponent), - multi: true - }] + selector: 'tb-default-tenant-profile-configuration', + templateUrl: './default-tenant-profile-configuration.component.html', + styleUrls: ['./default-tenant-profile-configuration.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultTenantProfileConfigurationComponent), + multi: true + }], + standalone: false }) export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.ts index abca908623..a7ada0090b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.ts @@ -29,7 +29,8 @@ export interface RateLimitsDetailsDialogData { } @Component({ - templateUrl: './rate-limits-details-dialog.component.html' + templateUrl: './rate-limits-details-dialog.component.html', + standalone: false }) export class RateLimitsDetailsDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.ts index 4434259bb4..7b58242e4d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.ts @@ -33,21 +33,22 @@ import { isDefinedAndNotNull } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-rate-limits-list', - templateUrl: './rate-limits-list.component.html', - styleUrls: ['./rate-limits-list.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RateLimitsListComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => RateLimitsListComponent), - multi: true - } - ] + selector: 'tb-rate-limits-list', + templateUrl: './rate-limits-list.component.html', + styleUrls: ['./rate-limits-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RateLimitsListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RateLimitsListComponent), + multi: true + } + ], + standalone: false }) export class RateLimitsListComponent implements ControlValueAccessor, Validator, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.ts index 16b00f0d6e..d5195cbad9 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.ts @@ -19,9 +19,10 @@ import { TranslateService } from '@ngx-translate/core'; import { RateLimits, rateLimitsArrayToHtml } from './rate-limits.models'; @Component({ - selector: 'tb-rate-limits-text', - templateUrl: './rate-limits-text.component.html', - styleUrls: ['./rate-limits-text.component.scss'] + selector: 'tb-rate-limits-text', + templateUrl: './rate-limits-text.component.html', + styleUrls: ['./rate-limits-text.component.scss'], + standalone: false }) export class RateLimitsTextComponent implements OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.ts index 6e03646bba..397253b753 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.ts @@ -39,21 +39,22 @@ import { import { isDefined } from '@core/utils'; @Component({ - selector: 'tb-rate-limits', - templateUrl: './rate-limits.component.html', - styleUrls: ['./rate-limits.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RateLimitsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => RateLimitsComponent), - multi: true, - } - ] + selector: 'tb-rate-limits', + templateUrl: './rate-limits.component.html', + styleUrls: ['./rate-limits.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RateLimitsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RateLimitsComponent), + multi: true, + } + ], + standalone: false }) export class RateLimitsComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.ts index 0117706d12..a140a7053d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.ts @@ -24,14 +24,15 @@ import { TenantProfileConfiguration, TenantProfileType } from '@shared/models/te import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-tenant-profile-configuration', - templateUrl: './tenant-profile-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TenantProfileConfigurationComponent), - multi: true - }] + selector: 'tb-tenant-profile-configuration', + templateUrl: './tenant-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileConfigurationComponent), + multi: true + }], + standalone: false }) export class TenantProfileConfigurationComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts index 2760226998..e3b22095c4 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts @@ -38,21 +38,22 @@ import { Subscription } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-queue-form', - templateUrl: './queue-form.component.html', - styleUrls: ['./queue-form.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => QueueFormComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => QueueFormComponent), - multi: true, - } - ] + selector: 'tb-queue-form', + templateUrl: './queue-form.component.html', + styleUrls: ['./queue-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => QueueFormComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => QueueFormComponent), + multi: true, + } + ], + standalone: false }) export class QueueFormComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts index c9a4fedb69..362bb73dab 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts @@ -44,10 +44,11 @@ export interface RelationDialogData { } @Component({ - selector: 'tb-relation-dialog', - templateUrl: './relation-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: RelationDialogComponent}], - styleUrls: ['./relation-dialog.component.scss'] + selector: 'tb-relation-dialog', + templateUrl: './relation-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: RelationDialogComponent }], + styleUrls: ['./relation-dialog.component.scss'], + standalone: false }) export class RelationDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts index 76ca033183..f8e1e796a5 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts @@ -33,16 +33,17 @@ import { takeUntil } from 'rxjs/operators'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-relation-filters', - templateUrl: './relation-filters.component.html', - styleUrls: ['./relation-filters.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RelationFiltersComponent), - multi: true - } - ] + selector: 'tb-relation-filters', + templateUrl: './relation-filters.component.html', + styleUrls: ['./relation-filters.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelationFiltersComponent), + multi: true + } + ], + standalone: false }) export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index f5dcd97032..00b10e401a 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -52,10 +52,11 @@ import { hidePageSizePixelValue } from '@shared/models/constants'; import { FormBuilder } from '@angular/forms'; @Component({ - selector: 'tb-relation-table', - templateUrl: './relation-table.component.html', - styleUrls: ['./relation-table.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-relation-table', + templateUrl: './relation-table.component.html', + styleUrls: ['./relation-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class RelationTableComponent extends PageComponent implements AfterViewInit, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts index 5456fd7f88..a674ba663b 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts @@ -34,10 +34,11 @@ export interface ResourcesDialogData { } @Component({ - selector: 'tb-resources-dialog', - templateUrl: './resources-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: ResourcesDialogComponent}], - styleUrls: ['./resources-dialog.component.scss'] + selector: 'tb-resources-dialog', + templateUrl: './resources-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: ResourcesDialogComponent }], + styleUrls: ['./resources-dialog.component.scss'], + standalone: false }) export class ResourcesDialogComponent extends DialogComponent implements ErrorStateMatcher, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts index b826bc6616..535eaf2e5e 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts @@ -35,8 +35,9 @@ import { isDefinedAndNotNull } from '@core/utils'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; @Component({ - selector: 'tb-resources-library', - templateUrl: './resources-library.component.html' + selector: 'tb-resources-library', + templateUrl: './resources-library.component.html', + standalone: false }) export class ResourcesLibraryComponent extends EntityComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/router-tabs.component.ts b/ui-ngx/src/app/modules/home/components/router-tabs.component.ts index a4e6a883b6..b75b90e373 100644 --- a/ui-ngx/src/app/modules/home/components/router-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/components/router-tabs.component.ts @@ -28,9 +28,10 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @Component({ - selector: 'tb-router-tabs', - templateUrl: './router-tabs.component.html', - styleUrls: ['./router-tabs.component.scss'] + selector: 'tb-router-tabs', + templateUrl: './router-tabs.component.html', + styleUrls: ['./router-tabs.component.scss'], + standalone: false }) export class RouterTabsComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts index e6176603c3..b0d83cac46 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts @@ -33,14 +33,15 @@ import { RuleChainType } from '@app/shared/models/rule-chain.models'; import { getEntityDetailsPageURL } from '@core/utils'; @Component({ - selector: 'tb-rule-chain-autocomplete', - templateUrl: './rule-chain-autocomplete.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RuleChainAutocompleteComponent), - multi: true - }] + selector: 'tb-rule-chain-autocomplete', + templateUrl: './rule-chain-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleChainAutocompleteComponent), + multi: true + }], + standalone: false }) export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts index 7e73655529..22cdec0c82 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts @@ -34,17 +34,18 @@ import { isDefinedAndNotNull } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-advanced-processing-setting-row', - templateUrl: './advanced-processing-setting-row.component.html', - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AdvancedProcessingSettingRowComponent), - multi: true - },{ - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AdvancedProcessingSettingRowComponent), - multi: true - }] + selector: 'tb-advanced-processing-setting-row', + templateUrl: './advanced-processing-setting-row.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AdvancedProcessingSettingRowComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AdvancedProcessingSettingRowComponent), + multi: true + }], + standalone: false }) export class AdvancedProcessingSettingRowComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.ts index 3cd880cedd..6a8ef33a56 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.ts @@ -30,17 +30,18 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { AttributeAdvancedProcessingStrategy } from '@home/components/rule-node/action/attributes-config.model'; @Component({ - selector: 'tb-advanced-processing-settings', - templateUrl: './advanced-processing-setting.component.html', - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AdvancedProcessingSettingComponent), - multi: true - },{ - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AdvancedProcessingSettingComponent), - multi: true - }] + selector: 'tb-advanced-processing-settings', + templateUrl: './advanced-processing-setting.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AdvancedProcessingSettingComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AdvancedProcessingSettingComponent), + multi: true + }], + standalone: false }) export class AdvancedProcessingSettingComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.ts index 0a67f7ba56..c844cd114b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-assign-to-customer-config', - templateUrl: './assign-customer-config.component.html', - styleUrls: [] + selector: 'tb-action-node-assign-to-customer-config', + templateUrl: './assign-customer-config.component.html', + styleUrls: [], + standalone: false }) export class AssignCustomerConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts index 13deb3afda..ab43627104 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts @@ -33,9 +33,10 @@ import { } from '@home/components/rule-node/action/attributes-config.model'; @Component({ - selector: 'tb-action-node-attributes-config', - templateUrl: './attributes-config.component.html', - styleUrls: [] + selector: 'tb-action-node-attributes-config', + templateUrl: './attributes-config.component.html', + styleUrls: [], + standalone: false }) export class AttributesConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.ts index fdc7883fec..812f50955a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.ts @@ -28,9 +28,10 @@ import type { JsFuncComponent } from '@app/shared/components/js-func.component'; import { DebugRuleNodeEventBody } from '@shared/models/event.models'; @Component({ - selector: 'tb-action-node-clear-alarm-config', - templateUrl: './clear-alarm-config.component.html', - styleUrls: [] + selector: 'tb-action-node-clear-alarm-config', + templateUrl: './clear-alarm-config.component.html', + styleUrls: [], + standalone: false }) export class ClearAlarmConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts index f4ec7424aa..cd2122a8dd 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts @@ -32,9 +32,10 @@ import { DebugRuleNodeEventBody } from '@shared/models/event.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-action-node-create-alarm-config', - templateUrl: './create-alarm-config.component.html', - styleUrls: [] + selector: 'tb-action-node-create-alarm-config', + templateUrl: './create-alarm-config.component.html', + styleUrls: [], + standalone: false }) export class CreateAlarmConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts index 40fce452a9..5d3e85e565 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts @@ -21,9 +21,10 @@ import { EntitySearchDirection } from '@app/shared/models/relation.models'; import { EntityType } from '@app/shared/models/entity-type.models'; @Component({ - selector: 'tb-action-node-create-relation-config', - templateUrl: './create-relation-config.component.html', - styleUrls: [] + selector: 'tb-action-node-create-relation-config', + templateUrl: './create-relation-config.component.html', + styleUrls: [], + standalone: false }) export class CreateRelationConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts index 79e27e594f..565b5e9dea 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts @@ -23,9 +23,10 @@ import { AttributeScope, telemetryTypeTranslations } from '@shared/models/teleme import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-action-node-delete-attributes-config', - templateUrl: './delete-attributes-config.component.html', - styleUrls: [] + selector: 'tb-action-node-delete-attributes-config', + templateUrl: './delete-attributes-config.component.html', + styleUrls: [], + standalone: false }) export class DeleteAttributesConfigComponent extends RuleNodeConfigurationComponent { @ViewChild('attributeChipList') attributeChipList: MatChipGrid; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts index d7839e1f23..606035bd08 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts @@ -21,9 +21,10 @@ import { EntitySearchDirection } from '@app/shared/models/relation.models'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-delete-relation-config', - templateUrl: './delete-relation-config.component.html', - styleUrls: [] + selector: 'tb-action-node-delete-relation-config', + templateUrl: './delete-relation-config.component.html', + styleUrls: [], + standalone: false }) export class DeleteRelationConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/device-profile-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/device-profile-config.component.ts index 3bd49c65f2..e7397f431c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/device-profile-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/device-profile-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-device-profile-config', - templateUrl: './device-profile-config.component.html', - styleUrls: [] + selector: 'tb-action-node-device-profile-config', + templateUrl: './device-profile-config.component.html', + styleUrls: [], + standalone: false }) export class DeviceProfileConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.ts index f3176849f1..08a81b8bbb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.ts @@ -27,7 +27,8 @@ import { @Component({ selector: 'tb-action-node-device-state-config', templateUrl: './device-state-config.component.html', - styleUrls: [] + styleUrls: [], + standalone: false }) export class DeviceStateConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.ts index 2632a080a6..a421400791 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.ts @@ -28,9 +28,10 @@ import { EntityType } from '@app/shared/models/entity-type.models'; import { DebugRuleNodeEventBody } from '@shared/models/event.models'; @Component({ - selector: 'tb-action-node-generator-config', - templateUrl: './generator-config.component.html', - styleUrls: ['generator-config.component.scss'] + selector: 'tb-action-node-generator-config', + templateUrl: './generator-config.component.html', + styleUrls: ['generator-config.component.scss'], + standalone: false }) export class GeneratorConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.ts index c16a13c5d8..b74efbe677 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.ts @@ -28,9 +28,10 @@ import { } from '../rule-node-config.models'; @Component({ - selector: 'tb-action-node-gps-geofencing-config', - templateUrl: './gps-geo-action-config.component.html', - styleUrls: ['./gps-geo-action-config.component.scss'] + selector: 'tb-action-node-gps-geofencing-config', + templateUrl: './gps-geo-action-config.component.html', + styleUrls: ['./gps-geo-action-config.component.scss'], + standalone: false }) export class GpsGeoActionConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/log-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/log-config.component.ts index 4be9783e6a..7d62fdb7d4 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/log-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/log-config.component.ts @@ -27,9 +27,10 @@ import type { JsFuncComponent } from '@app/shared/components/js-func.component'; import { DebugRuleNodeEventBody } from '@shared/models/event.models'; @Component({ - selector: 'tb-action-node-log-config', - templateUrl: './log-config.component.html', - styleUrls: [] + selector: 'tb-action-node-log-config', + templateUrl: './log-config.component.html', + styleUrls: [], + standalone: false }) export class LogConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.ts index b7579e52a6..66c962a4e7 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.ts @@ -27,9 +27,10 @@ import { @Component({ - selector: 'tb-action-node-math-function-config', - templateUrl: './math-function-config.component.html', - styleUrls: ['./math-function-config.component.scss'] + selector: 'tb-action-node-math-function-config', + templateUrl: './math-function-config.component.html', + styleUrls: ['./math-function-config.component.scss'], + standalone: false }) export class MathFunctionConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.ts index 1cdff79ff6..ab9f1186f6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-msg-count-config', - templateUrl: './msg-count-config.component.html', - styleUrls: [] + selector: 'tb-action-node-msg-count-config', + templateUrl: './msg-count-config.component.html', + styleUrls: [], + standalone: false }) export class MsgCountConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.ts index 09ce257e94..c36ad95a58 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-msg-delay-config', - templateUrl: './msg-delay-config.component.html', - styleUrls: [] + selector: 'tb-action-node-msg-delay-config', + templateUrl: './msg-delay-config.component.html', + styleUrls: [], + standalone: false }) export class MsgDelayConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.ts index 451caa2301..a7a5542b98 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.ts @@ -20,9 +20,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m import { AttributeScope, telemetryTypeTranslations } from '@shared/models/telemetry/telemetry.models'; @Component({ - selector: 'tb-action-node-push-to-cloud-config', - templateUrl: './push-to-cloud-config.component.html', - styleUrls: [] + selector: 'tb-action-node-push-to-cloud-config', + templateUrl: './push-to-cloud-config.component.html', + styleUrls: [], + standalone: false }) export class PushToCloudConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.ts index 59a9f539cb..5192161cc8 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.ts @@ -20,9 +20,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m import { AttributeScope, telemetryTypeTranslations } from '@shared/models/telemetry/telemetry.models'; @Component({ - selector: 'tb-action-node-push-to-edge-config', - templateUrl: './push-to-edge-config.component.html', - styleUrls: [] + selector: 'tb-action-node-push-to-edge-config', + templateUrl: './push-to-edge-config.component.html', + styleUrls: [], + standalone: false }) export class PushToEdgeConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-reply-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-reply-config.component.ts index 016373c78b..53b1268189 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-reply-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-reply-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-rpc-reply-config', - templateUrl: './rpc-reply-config.component.html', - styleUrls: [] + selector: 'tb-action-node-rpc-reply-config', + templateUrl: './rpc-reply-config.component.html', + styleUrls: [], + standalone: false }) export class RpcReplyConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-request-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-request-config.component.ts index f1dbc1db86..546f344863 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-request-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-request-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-rpc-request-config', - templateUrl: './rpc-request-config.component.html', - styleUrls: [] + selector: 'tb-action-node-rpc-request-config', + templateUrl: './rpc-request-config.component.html', + styleUrls: [], + standalone: false }) export class RpcRequestConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.ts index 03f9dd7382..ac7cda0abc 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-custom-table-config', - templateUrl: './save-to-custom-table-config.component.html', - styleUrls: [] + selector: 'tb-action-node-custom-table-config', + templateUrl: './save-to-custom-table-config.component.html', + styleUrls: [], + standalone: false }) export class SaveToCustomTableConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/send-rest-api-call-reply-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/send-rest-api-call-reply-config.component.ts index 1c825aba3c..b3d2887be5 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/send-rest-api-call-reply-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/send-rest-api-call-reply-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-send-rest-api-call-reply-config', - templateUrl: './send-rest-api-call-reply-config.component.html', - styleUrls: [] + selector: 'tb-action-node-send-rest-api-call-reply-config', + templateUrl: './send-rest-api-call-reply-config.component.html', + styleUrls: [], + standalone: false }) export class SendRestApiCallReplyConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.ts index 1349eeb963..93427afeda 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.ts @@ -29,9 +29,10 @@ import { } from '@home/components/rule-node/action/timeseries-config.models'; @Component({ - selector: 'tb-action-node-timeseries-config', - templateUrl: './timeseries-config.component.html', - styleUrls: [] + selector: 'tb-action-node-timeseries-config', + templateUrl: './timeseries-config.component.html', + styleUrls: [], + standalone: false }) export class TimeseriesConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/unassign-customer-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/unassign-customer-config.component.ts index 5fe0c5c1df..ee5c1a5031 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/unassign-customer-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/unassign-customer-config.component.ts @@ -20,9 +20,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; @Component({ - selector: 'tb-action-node-un-assign-to-customer-config', - templateUrl: './unassign-customer-config.component.html', - styleUrls: [] + selector: 'tb-action-node-un-assign-to-customer-config', + templateUrl: './unassign-customer-config.component.html', + styleUrls: [], + standalone: false }) export class UnassignCustomerConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/alarm-status-select.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/alarm-status-select.component.ts index bc9de1be09..2ccceb8877 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/alarm-status-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/alarm-status-select.component.ts @@ -20,14 +20,15 @@ import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-alarm-status-select', - templateUrl: './alarm-status-select.component.html', - styleUrls: ['./alarm-status-select.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AlarmStatusSelectComponent), - multi: true - }] + selector: 'tb-alarm-status-select', + templateUrl: './alarm-status-select.component.html', + styleUrls: ['./alarm-status-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmStatusSelectComponent), + multi: true + }], + standalone: false }) export class AlarmStatusSelectComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.ts index 60bf03607d..dd46d40b5e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.ts @@ -39,21 +39,22 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-arguments-map-config', - templateUrl: './arguments-map-config.component.html', - styleUrls: ['./arguments-map-config.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ArgumentsMapConfigComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ArgumentsMapConfigComponent), - multi: true, - } - ] + selector: 'tb-arguments-map-config', + templateUrl: './arguments-map-config.component.html', + styleUrls: ['./arguments-map-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ArgumentsMapConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ArgumentsMapConfigComponent), + multi: true, + } + ], + standalone: false }) export class ArgumentsMapConfigComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.ts index 1a8556930c..941ccb2706 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.ts @@ -45,21 +45,22 @@ interface CredentialsConfig { } @Component({ - selector: 'tb-credentials-config', - templateUrl: './credentials-config.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CredentialsConfigComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CredentialsConfigComponent), - multi: true, - } - ] + selector: 'tb-credentials-config', + templateUrl: './credentials-config.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CredentialsConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CredentialsConfigComponent), + multi: true, + } + ], + standalone: false }) export class CredentialsConfigComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.ts index c99f9d7848..2b55bf2342 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.ts @@ -31,16 +31,17 @@ interface DeviceRelationsQuery { } @Component({ - selector: 'tb-device-relations-query-config', - templateUrl: './device-relations-query-config.component.html', - styleUrls: ['./device-relations-query-config.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceRelationsQueryConfigComponent), - multi: true - } - ] + selector: 'tb-device-relations-query-config', + templateUrl: './device-relations-query-config.component.html', + styleUrls: ['./device-relations-query-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceRelationsQueryConfigComponent), + multi: true + } + ], + standalone: false }) export class DeviceRelationsQueryConfigComponent extends PageComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.ts index c40ab0ff92..1159d1ca9b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.ts @@ -17,9 +17,10 @@ import { Component, Input } from '@angular/core'; @Component({ - selector: 'tb-example-hint', - templateUrl: './example-hint.component.html', - styleUrls: [] + selector: 'tb-example-hint', + templateUrl: './example-hint.component.html', + styleUrls: [], + standalone: false }) export class ExampleHintComponent { @Input() hintText: string; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.ts index 30d96016bc..33e4a203a7 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.ts @@ -33,21 +33,22 @@ import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-kv-map-config-old', - templateUrl: './kv-map-config-old.component.html', - styleUrls: ['./kv-map-config-old.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => KvMapConfigOldComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => KvMapConfigOldComponent), - multi: true, - } - ] + selector: 'tb-kv-map-config-old', + templateUrl: './kv-map-config-old.component.html', + styleUrls: ['./kv-map-config-old.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KvMapConfigOldComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KvMapConfigOldComponent), + multi: true, + } + ], + standalone: false }) export class KvMapConfigOldComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config.component.ts index 9cfb50961f..e98f145724 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config.component.ts @@ -34,21 +34,22 @@ import { isEqual } from '@core/public-api'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-kv-map-config', - templateUrl: './kv-map-config.component.html', - styleUrls: ['./kv-map-config.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => KvMapConfigComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => KvMapConfigComponent), - multi: true, - } - ] + selector: 'tb-kv-map-config', + templateUrl: './kv-map-config.component.html', + styleUrls: ['./kv-map-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KvMapConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KvMapConfigComponent), + multi: true, + } + ], + standalone: false }) export class KvMapConfigComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.ts index 917c4c7a68..6f540b6181 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.ts @@ -23,16 +23,17 @@ import { map, tap } from 'rxjs/operators'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; @Component({ - selector: 'tb-math-function-autocomplete', - templateUrl: './math-function-autocomplete.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MathFunctionAutocompleteComponent), - multi: true - } - ] + selector: 'tb-math-function-autocomplete', + templateUrl: './math-function-autocomplete.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MathFunctionAutocompleteComponent), + multi: true + } + ], + standalone: false }) export class MathFunctionAutocompleteComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.ts index 6ff08c5f7e..b41af5c78b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.ts @@ -26,16 +26,17 @@ import { TranslateService } from '@ngx-translate/core'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; @Component({ - selector: 'tb-message-types-config', - templateUrl: './message-types-config.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MessageTypesConfigComponent), - multi: true - } - ] + selector: 'tb-message-types-config', + templateUrl: './message-types-config.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MessageTypesConfigComponent), + multi: true + } + ], + standalone: false }) export class MessageTypesConfigComponent extends PageComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.ts index 2d31d016ac..1aca581efc 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.ts @@ -21,13 +21,14 @@ import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-msg-metadata-chip', - templateUrl: './msg-metadata-chip.component.html', - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MsgMetadataChipComponent), - multi: true - }] + selector: 'tb-msg-metadata-chip', + templateUrl: './msg-metadata-chip.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MsgMetadataChipComponent), + multi: true + }], + standalone: false }) export class MsgMetadataChipComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts index cf9f54a35a..5cb9922fcb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts @@ -34,21 +34,22 @@ interface MessageType { } @Component({ - selector: 'tb-output-message-type-autocomplete', - templateUrl: './output-message-type-autocomplete.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => OutputMessageTypeAutocompleteComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => OutputMessageTypeAutocompleteComponent), - multi: true - } - ] + selector: 'tb-output-message-type-autocomplete', + templateUrl: './output-message-type-autocomplete.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => OutputMessageTypeAutocompleteComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => OutputMessageTypeAutocompleteComponent), + multi: true + } + ], + standalone: false }) export class OutputMessageTypeAutocompleteComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts index c209df227a..83ed4a435f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts @@ -22,16 +22,17 @@ import { RelationsQuery } from '../rule-node-config.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-relations-query-config-old', - templateUrl: './relations-query-config-old.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RelationsQueryConfigOldComponent), - multi: true - } - ] + selector: 'tb-relations-query-config-old', + templateUrl: './relations-query-config-old.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelationsQueryConfigOldComponent), + multi: true + } + ], + standalone: false }) export class RelationsQueryConfigOldComponent extends PageComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.ts index a06aeec2c3..cc283afe27 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.ts @@ -22,15 +22,16 @@ import { RelationsQuery } from '../rule-node-config.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-relations-query-config', - templateUrl: './relations-query-config.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RelationsQueryConfigComponent), - multi: true - } - ] + selector: 'tb-relations-query-config', + templateUrl: './relations-query-config.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelationsQueryConfigComponent), + multi: true + } + ], + standalone: false }) export class RelationsQueryConfigComponent extends PageComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.ts index 10a8658490..bc7bc01a55 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.ts @@ -31,18 +31,19 @@ import { isDefinedAndNotNull } from '@core/public-api'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-select-attributes', - templateUrl: './select-attributes.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SelectAttributesComponent), - multi: true - }, { - provide: NG_VALIDATORS, - useExisting: SelectAttributesComponent, - multi: true - }] + selector: 'tb-select-attributes', + templateUrl: './select-attributes.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SelectAttributesComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: SelectAttributesComponent, + multi: true + }], + standalone: false }) export class SelectAttributesComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.ts index 61807afa1e..558aabe9ba 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.ts @@ -36,21 +36,22 @@ import { OriginatorFieldsMappingValues, SvMapOption } from '../rule-node-config. import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-sv-map-config', - templateUrl: './sv-map-config.component.html', - styleUrls: ['./sv-map-config.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SvMapConfigComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => SvMapConfigComponent), - multi: true, - } - ] + selector: 'tb-sv-map-config', + templateUrl: './sv-map-config.component.html', + styleUrls: ['./sv-map-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SvMapConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SvMapConfigComponent), + multi: true, + } + ], + standalone: false }) export class SvMapConfigComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/empty-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/empty-config.component.ts index 06747e23cf..554fbf0c7b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/empty-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/empty-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-node-empty-config', - template: '
', - styleUrls: [] + selector: 'tb-node-empty-config', + template: '
', + styleUrls: [], + standalone: false }) export class EmptyConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.ts index a5fdc62b1a..c7df00c008 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.ts @@ -22,8 +22,9 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m import { deepTrim, isDefinedAndNotNull } from '@app/core/utils'; @Component({ - selector: 'tb-enrichment-node-calculate-delta-config', - templateUrl: './calculate-delta-config.component.html' + selector: 'tb-enrichment-node-calculate-delta-config', + templateUrl: './calculate-delta-config.component.html', + standalone: false }) export class CalculateDeltaConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.ts index ae8fb3b1b9..757b9d5add 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.ts @@ -22,9 +22,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { DataToFetch, dataToFetchTranslations, FetchTo } from '@home/components/rule-node/rule-node-config.models'; @Component({ - selector: 'tb-enrichment-node-customer-attributes-config', - templateUrl: './customer-attributes-config.component.html', - styleUrls: ['./customer-attributes-config.component.scss'] + selector: 'tb-enrichment-node-customer-attributes-config', + templateUrl: './customer-attributes-config.component.html', + styleUrls: ['./customer-attributes-config.component.scss'], + standalone: false }) export class CustomerAttributesConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/device-attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/device-attributes-config.component.ts index c05119c0f5..0520ed1d48 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/device-attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/device-attributes-config.component.ts @@ -22,9 +22,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { FetchTo } from '@home/components/rule-node/rule-node-config.models'; @Component({ - selector: 'tb-enrichment-node-device-attributes-config', - templateUrl: './device-attributes-config.component.html', - styleUrls: [] + selector: 'tb-enrichment-node-device-attributes-config', + templateUrl: './device-attributes-config.component.html', + styleUrls: [], + standalone: false }) export class DeviceAttributesConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.ts index 02cd9192b8..2f558dbc45 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.ts @@ -26,9 +26,10 @@ import { } from '@home/components/rule-node/rule-node-config.models'; @Component({ - selector: 'tb-enrichment-node-entity-details-config', - templateUrl: './entity-details-config.component.html', - styleUrls: [] + selector: 'tb-enrichment-node-entity-details-config', + templateUrl: './entity-details-config.component.html', + styleUrls: [], + standalone: false }) export class EntityDetailsConfigComponent extends RuleNodeConfigurationComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/fetch-device-credentials-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/fetch-device-credentials-config.component.ts index 25e6a209ed..2ca92eb72c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/fetch-device-credentials-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/fetch-device-credentials-config.component.ts @@ -21,8 +21,9 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m import { FetchTo } from '@home/components/rule-node/rule-node-config.models'; @Component({ - selector: 'tb-enrichment-node-fetch-device-credentials-config', - templateUrl: './fetch-device-credentials-config.component.html' + selector: 'tb-enrichment-node-fetch-device-credentials-config', + templateUrl: './fetch-device-credentials-config.component.html', + standalone: false }) export class FetchDeviceCredentialsConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/get-telemetry-from-database-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/get-telemetry-from-database-config.component.ts index 148cc1b758..cea4771021 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/get-telemetry-from-database-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/get-telemetry-from-database-config.component.ts @@ -31,9 +31,10 @@ import { } from '../rule-node-config.models'; @Component({ - selector: 'tb-enrichment-node-get-telemetry-from-database', - templateUrl: './get-telemetry-from-database-config.component.html', - styleUrls: ['./get-telemetry-from-database-config.component.scss'] + selector: 'tb-enrichment-node-get-telemetry-from-database', + templateUrl: './get-telemetry-from-database-config.component.html', + styleUrls: ['./get-telemetry-from-database-config.component.scss'], + standalone: false }) export class GetTelemetryFromDatabaseConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/originator-attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/originator-attributes-config.component.ts index 2d6f5bd663..57fa20dc21 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/originator-attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/originator-attributes-config.component.ts @@ -22,9 +22,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { FetchTo } from '@home/components/rule-node/rule-node-config.models'; @Component({ - selector: 'tb-enrichment-node-originator-attributes-config', - templateUrl: './originator-attributes-config.component.html', - styleUrls: [] + selector: 'tb-enrichment-node-originator-attributes-config', + templateUrl: './originator-attributes-config.component.html', + styleUrls: [], + standalone: false }) export class OriginatorAttributesConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/originator-fields-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/originator-fields-config.component.ts index 9173ada5ee..102d61fddf 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/originator-fields-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/originator-fields-config.component.ts @@ -22,8 +22,9 @@ import { allowedOriginatorFields, FetchTo, SvMapOption } from '@home/components/ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; @Component({ - selector: 'tb-enrichment-node-originator-fields-config', - templateUrl: './originator-fields-config.component.html' + selector: 'tb-enrichment-node-originator-fields-config', + templateUrl: './originator-fields-config.component.html', + standalone: false }) export class OriginatorFieldsConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.ts index 0927645a47..f5c6acd5a4 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.ts @@ -30,9 +30,10 @@ import { import { entityFields } from '@shared/models/entity.models'; @Component({ - selector: 'tb-enrichment-node-related-attributes-config', - templateUrl: './related-attributes-config.component.html', - styleUrls: [] + selector: 'tb-enrichment-node-related-attributes-config', + templateUrl: './related-attributes-config.component.html', + styleUrls: [], + standalone: false }) export class RelatedAttributesConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/tenant-attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/tenant-attributes-config.component.ts index 2d29081184..9635d2d45f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/tenant-attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/tenant-attributes-config.component.ts @@ -22,9 +22,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { DataToFetch, dataToFetchTranslations, FetchTo } from '../rule-node-config.models'; @Component({ - selector: 'tb-enrichment-node-tenant-attributes-config', - templateUrl: './tenant-attributes-config.component.html', - styleUrls: ['./tenant-attributes-config.component.scss'] + selector: 'tb-enrichment-node-tenant-attributes-config', + templateUrl: './tenant-attributes-config.component.html', + styleUrls: ['./tenant-attributes-config.component.scss'], + standalone: false }) export class TenantAttributesConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index a01f95549b..682c67695f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -28,9 +28,10 @@ import { Resource, ResourceType } from "@shared/models/resource.models"; import { ResourcesDialogComponent, ResourcesDialogData } from "@home/components/resources/resources-dialog.component"; @Component({ - selector: 'tb-external-node-ai-config', - templateUrl: './ai-config.component.html', - styleUrls: [] + selector: 'tb-external-node-ai-config', + templateUrl: './ai-config.component.html', + styleUrls: [], + standalone: false }) export class AiConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts index 772f411c44..ac7ecd58cb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts @@ -25,9 +25,10 @@ import { import { MqttVersion } from '@shared/models/mqtt.models'; @Component({ - selector: 'tb-external-node-azure-iot-hub-config', - templateUrl: './azure-iot-hub-config.component.html', - styleUrls: ['./mqtt-config.component.scss'] + selector: 'tb-external-node-azure-iot-hub-config', + templateUrl: './azure-iot-hub-config.component.html', + styleUrls: ['./mqtt-config.component.scss'], + standalone: false }) export class AzureIotHubConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.ts index 41fd064314..3b8b07b108 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.ts @@ -23,9 +23,10 @@ import { } from '@home/components/rule-node/rule-node-config.models'; @Component({ - selector: 'tb-external-node-kafka-config', - templateUrl: './kafka-config.component.html', - styleUrls: [] + selector: 'tb-external-node-kafka-config', + templateUrl: './kafka-config.component.html', + styleUrls: [], + standalone: false }) export class KafkaConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.ts index eccbad5295..914ed3522c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-external-node-lambda-config', - templateUrl: './lambda-config.component.html', - styleUrls: [] + selector: 'tb-external-node-lambda-config', + templateUrl: './lambda-config.component.html', + styleUrls: [], + standalone: false }) export class LambdaConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/mqtt-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/mqtt-config.component.ts index bf0f5d174a..1fe03704dd 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/mqtt-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/mqtt-config.component.ts @@ -20,9 +20,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-external-node-mqtt-config', - templateUrl: './mqtt-config.component.html', - styleUrls: ['./mqtt-config.component.scss'] + selector: 'tb-external-node-mqtt-config', + templateUrl: './mqtt-config.component.html', + styleUrls: ['./mqtt-config.component.scss'], + standalone: false }) export class MqttConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.ts index 6ee6c0525f..386d85ac1a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.ts @@ -21,9 +21,10 @@ import { NotificationType } from '@shared/models/notification.models'; import { EntityType } from '@shared/models/entity-type.models'; @Component({ - selector: 'tb-external-node-notification-config', - templateUrl: './notification-config.component.html', - styleUrls: [] + selector: 'tb-external-node-notification-config', + templateUrl: './notification-config.component.html', + styleUrls: [], + standalone: false }) export class NotificationConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/pubsub-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/pubsub-config.component.ts index 652b8dca5b..4a7d9f972f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/pubsub-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/pubsub-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-external-node-pub-sub-config', - templateUrl: './pubsub-config.component.html', - styleUrls: [] + selector: 'tb-external-node-pub-sub-config', + templateUrl: './pubsub-config.component.html', + styleUrls: [], + standalone: false }) export class PubSubConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.ts index d4fdc4601a..2f44901d52 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-external-node-rabbit-mq-config', - templateUrl: './rabbit-mq-config.component.html', - styleUrls: [] + selector: 'tb-external-node-rabbit-mq-config', + templateUrl: './rabbit-mq-config.component.html', + styleUrls: [], + standalone: false }) export class RabbitMqConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts index 3be6a6499b..4723a0c510 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts @@ -20,9 +20,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { HttpRequestType, IntLimit } from '../rule-node-config.models'; @Component({ - selector: 'tb-external-node-rest-api-call-config', - templateUrl: './rest-api-call-config.component.html', - styleUrls: [] + selector: 'tb-external-node-rest-api-call-config', + templateUrl: './rest-api-call-config.component.html', + styleUrls: [], + standalone: false }) export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.ts index fd2b587446..98bbdc0ff5 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-external-node-send-email-config', - templateUrl: './send-email-config.component.html', - styleUrls: [] + selector: 'tb-external-node-send-email-config', + templateUrl: './send-email-config.component.html', + styleUrls: [], + standalone: false }) export class SendEmailConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.ts index dbf8afb6c6..38c1cee016 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-external-node-send-sms-config', - templateUrl: './send-sms-config.component.html', - styleUrls: [] + selector: 'tb-external-node-send-sms-config', + templateUrl: './send-sms-config.component.html', + styleUrls: [], + standalone: false }) export class SendSmsConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.ts index 3d719577a7..a0e584e672 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.ts @@ -20,9 +20,10 @@ import { RuleNodeConfiguration, SlackChanelType, SlackChanelTypesTranslateMap } import { RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-external-node-slack-config', - templateUrl: './slack-config.component.html', - styleUrls: ['./slack-config.component.scss'] + selector: 'tb-external-node-slack-config', + templateUrl: './slack-config.component.html', + styleUrls: ['./slack-config.component.scss'], + standalone: false }) export class SlackConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/sns-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/sns-config.component.ts index 352fda3d70..cc9f18c181 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/sns-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/sns-config.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-external-node-sns-config', - templateUrl: './sns-config.component.html', - styleUrls: [] + selector: 'tb-external-node-sns-config', + templateUrl: './sns-config.component.html', + styleUrls: [], + standalone: false }) export class SnsConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts index 7170db813e..ee6cfac5da 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts @@ -20,9 +20,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { SqsQueueType, sqsQueueTypeTranslations } from '@home/components/rule-node/rule-node-config.models'; @Component({ - selector: 'tb-external-node-sqs-config', - templateUrl: './sqs-config.component.html', - styleUrls: [] + selector: 'tb-external-node-sqs-config', + templateUrl: './sqs-config.component.html', + styleUrls: [], + standalone: false }) export class SqsConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-alarm-status.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-alarm-status.component.ts index 082b6c0585..b64f9374fc 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-alarm-status.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-alarm-status.component.ts @@ -20,9 +20,10 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; @Component({ - selector: 'tb-filter-node-check-alarm-status-config', - templateUrl: './check-alarm-status.component.html', - styleUrls: [] + selector: 'tb-filter-node-check-alarm-status-config', + templateUrl: './check-alarm-status.component.html', + styleUrls: [], + standalone: false }) export class CheckAlarmStatusComponent extends RuleNodeConfigurationComponent { alarmStatusConfigForm: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.ts index 8bad5ac4ca..9f44bf83c1 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.ts @@ -20,9 +20,10 @@ import { FormBuilder, FormGroup, ValidationErrors, ValidatorFn, Validators } fro import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; @Component({ - selector: 'tb-filter-node-check-message-config', - templateUrl: './check-message-config.component.html', - styleUrls: [] + selector: 'tb-filter-node-check-message-config', + templateUrl: './check-message-config.component.html', + styleUrls: [], + standalone: false }) export class CheckMessageConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.ts index 09196196bf..a4678eef0e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.ts @@ -21,9 +21,10 @@ import { EntitySearchDirection, entitySearchDirectionTranslations } from '@app/s import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; @Component({ - selector: 'tb-filter-node-check-relation-config', - templateUrl: './check-relation-config.component.html', - styleUrls: ['./check-relation-config.component.scss'] + selector: 'tb-filter-node-check-relation-config', + templateUrl: './check-relation-config.component.html', + styleUrls: ['./check-relation-config.component.scss'], + standalone: false }) export class CheckRelationConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.ts index e92af96ee6..17aaf675f3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.ts @@ -21,9 +21,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { PerimeterType, perimeterTypeTranslations, RangeUnit, rangeUnitTranslations } from '../rule-node-config.models'; @Component({ - selector: 'tb-filter-node-gps-geofencing-config', - templateUrl: './gps-geo-filter-config.component.html', - styleUrls: ['./gps-geo-filter-config.component.scss'] + selector: 'tb-filter-node-gps-geofencing-config', + templateUrl: './gps-geo-filter-config.component.html', + styleUrls: ['./gps-geo-filter-config.component.scss'], + standalone: false }) export class GpsGeoFilterConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/message-type-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/filter/message-type-config.component.ts index 9145719950..34596c141c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/message-type-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/message-type-config.component.ts @@ -20,9 +20,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-filter-node-message-type-config', - templateUrl: './message-type-config.component.html', - styleUrls: [] + selector: 'tb-filter-node-message-type-config', + templateUrl: './message-type-config.component.html', + styleUrls: [], + standalone: false }) export class MessageTypeConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.ts index d72cc3231f..66d381075b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.ts @@ -21,9 +21,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { EntityType } from '@app/shared/models/entity-type.models'; @Component({ - selector: 'tb-filter-node-originator-type-config', - templateUrl: './originator-type-config.component.html', - styleUrls: [] + selector: 'tb-filter-node-originator-type-config', + templateUrl: './originator-type-config.component.html', + styleUrls: [], + standalone: false }) export class OriginatorTypeConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/script-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/filter/script-config.component.ts index 9bbc9601e7..9d1a7d511f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/script-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/script-config.component.ts @@ -24,9 +24,10 @@ import type { JsFuncComponent } from '@app/shared/components/js-func.component'; import { DebugRuleNodeEventBody } from '@app/shared/models/event.models'; @Component({ - selector: 'tb-filter-node-script-config', - templateUrl: './script-config.component.html', - styleUrls: [] + selector: 'tb-filter-node-script-config', + templateUrl: './script-config.component.html', + styleUrls: [], + standalone: false }) export class ScriptConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.ts index 26e6d0a21e..8a00fe94e8 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.ts @@ -27,9 +27,10 @@ import type { JsFuncComponent } from '@app/shared/components/js-func.component'; import { DebugRuleNodeEventBody } from '@shared/models/event.models'; @Component({ - selector: 'tb-filter-node-switch-config', - templateUrl: './switch-config.component.html', - styleUrls: [] + selector: 'tb-filter-node-switch-config', + templateUrl: './switch-config.component.html', + styleUrls: [], + standalone: false }) export class SwitchConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-input.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-input.component.ts index 87cf38bd7c..0403a37563 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-input.component.ts @@ -20,9 +20,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m import { EntityType } from '@shared/models/entity-type.models'; @Component({ - selector: 'tb-flow-node-rule-chain-input-config', - templateUrl: './rule-chain-input.component.html', - styleUrls: [] + selector: 'tb-flow-node-rule-chain-input-config', + templateUrl: './rule-chain-input.component.html', + styleUrls: [], + standalone: false }) export class RuleChainInputComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-output.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-output.component.ts index f4b2760f04..0505a2be7f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/flow/rule-chain-output.component.ts @@ -19,9 +19,10 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-flow-node-rule-chain-output-config', - templateUrl: './rule-chain-output.component.html', - styleUrls: [] + selector: 'tb-flow-node-rule-chain-output-config', + templateUrl: './rule-chain-output.component.html', + styleUrls: [], + standalone: false }) export class RuleChainOutputComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.ts index b5c63de344..3aeb18a453 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.ts @@ -25,8 +25,9 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { EntityType } from '@app/shared/models/entity-type.models'; @Component({ - selector: 'tb-transformation-node-change-originator-config', - templateUrl: './change-originator-config.component.html' + selector: 'tb-transformation-node-change-originator-config', + templateUrl: './change-originator-config.component.html', + standalone: false }) export class ChangeOriginatorConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.ts index 2f4bd029dc..e059a7de97 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.ts @@ -22,9 +22,10 @@ import { FetchFromToTranslation, FetchTo } from '../rule-node-config.models'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; @Component({ - selector: 'tb-transformation-node-copy-keys-config', - templateUrl: './copy-keys-config.component.html', - styleUrls: [] + selector: 'tb-transformation-node-copy-keys-config', + templateUrl: './copy-keys-config.component.html', + styleUrls: [], + standalone: false }) export class CopyKeysConfigComponent extends RuleNodeConfigurationComponent{ diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts index 4661084ef3..77c95515d3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts @@ -21,9 +21,10 @@ import { deduplicationStrategiesTranslations, FetchMode } from '@home/components import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-transformation-node-deduplication-config', - templateUrl: './deduplication-config.component.html', - styleUrls: [] + selector: 'tb-transformation-node-deduplication-config', + templateUrl: './deduplication-config.component.html', + styleUrls: [], + standalone: false }) export class DeduplicationConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.ts index bac37fb4f5..6d04408b75 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.ts @@ -22,9 +22,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar import { FetchTo, FetchToTranslation } from '@home/components/rule-node/rule-node-config.models'; @Component({ - selector: 'tb-transformation-node-delete-keys-config', - templateUrl: './delete-keys-config.component.html', - styleUrls: [] + selector: 'tb-transformation-node-delete-keys-config', + templateUrl: './delete-keys-config.component.html', + styleUrls: [], + standalone: false }) export class DeleteKeysConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/node-json-path-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/node-json-path-config.component.ts index e6e5d65d85..27719ccbb9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/node-json-path-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/node-json-path-config.component.ts @@ -19,9 +19,10 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-transformation-node-json-path-config', - templateUrl: './node-json-path-config.component.html', - styleUrls: [] + selector: 'tb-transformation-node-json-path-config', + templateUrl: './node-json-path-config.component.html', + styleUrls: [], + standalone: false }) export class NodeJsonPathConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.ts index c99e6029a5..fc6a677878 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.ts @@ -23,9 +23,10 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m @Component({ - selector: 'tb-transformation-node-rename-keys-config', - templateUrl: './rename-keys-config.component.html', - styleUrls: ['./rename-keys-config.component.scss'] + selector: 'tb-transformation-node-rename-keys-config', + templateUrl: './rename-keys-config.component.html', + styleUrls: ['./rename-keys-config.component.scss'], + standalone: false }) export class RenameKeysConfigComponent extends RuleNodeConfigurationComponent { renameKeysConfigForm: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.ts index 021bb21cb1..2007e06180 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.ts @@ -27,9 +27,10 @@ import { import { DebugRuleNodeEventBody } from '@shared/models/event.models'; @Component({ - selector: 'tb-transformation-node-script-config', - templateUrl: './script-config.component.html', - styleUrls: [] + selector: 'tb-transformation-node-script-config', + templateUrl: './script-config.component.html', + styleUrls: [], + standalone: false }) export class TransformScriptConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/to-email-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/to-email-config.component.ts index e2c7117ae0..21a1748085 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/to-email-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/to-email-config.component.ts @@ -20,9 +20,10 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shared/models/rule-node.models'; @Component({ - selector: 'tb-transformation-node-to-email-config', - templateUrl: './to-email-config.component.html', - styleUrls: ['./to-email-config.component.scss'] + selector: 'tb-transformation-node-to-email-config', + templateUrl: './to-email-config.component.html', + styleUrls: ['./to-email-config.component.scss'], + standalone: false }) export class ToEmailConfigComponent extends RuleNodeConfigurationComponent { diff --git a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.ts index 772ae7366a..d864c3ccfd 100644 --- a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.ts @@ -28,14 +28,15 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-aws-sns-provider-configuration', - templateUrl: './aws-sns-provider-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AwsSnsProviderConfigurationComponent), - multi: true - }] + selector: 'tb-aws-sns-provider-configuration', + templateUrl: './aws-sns-provider-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AwsSnsProviderConfigurationComponent), + multi: true + }], + standalone: false }) export class AwsSnsProviderConfigurationComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts index 04ea006af4..35111be91b 100644 --- a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts @@ -36,14 +36,15 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-smpp-sms-provider-configuration', - templateUrl: './smpp-sms-provider-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SmppSmsProviderConfigurationComponent), - multi: true - }] + selector: 'tb-smpp-sms-provider-configuration', + templateUrl: './smpp-sms-provider-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SmppSmsProviderConfigurationComponent), + multi: true + }], + standalone: false }) export class SmppSmsProviderConfigurationComponent implements ControlValueAccessor, OnInit{ diff --git a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts index 807b39f93f..adb477d8fb 100644 --- a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts @@ -34,14 +34,15 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-sms-provider-configuration', - templateUrl: './sms-provider-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SmsProviderConfigurationComponent), - multi: true - }] + selector: 'tb-sms-provider-configuration', + templateUrl: './sms-provider-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SmsProviderConfigurationComponent), + multi: true + }], + standalone: false }) export class SmsProviderConfigurationComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts index 550a2d8112..1041672c50 100644 --- a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts @@ -29,14 +29,15 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-twilio-sms-provider-configuration', - templateUrl: './twilio-sms-provider-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TwilioSmsProviderConfigurationComponent), - multi: true - }] + selector: 'tb-twilio-sms-provider-configuration', + templateUrl: './twilio-sms-provider-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TwilioSmsProviderConfigurationComponent), + multi: true + }], + standalone: false }) export class TwilioSmsProviderConfigurationComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts index 2f07e2b55c..a2a87edde0 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts @@ -34,9 +34,10 @@ import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.m import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @Component({ - selector: 'tb-auto-commit-settings', - templateUrl: './auto-commit-settings.component.html', - styleUrls: ['./auto-commit-settings.component.scss', './../../pages/admin/settings-card.scss'] + selector: 'tb-auto-commit-settings', + templateUrl: './auto-commit-settings.component.html', + styleUrls: ['./auto-commit-settings.component.scss', './../../pages/admin/settings-card.scss'], + standalone: false }) export class AutoCommitSettingsComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts index 21ac0825c9..af80a5be3a 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts @@ -37,9 +37,10 @@ import { share } from 'rxjs/operators'; import { parseHttpErrorMessage } from '@core/utils'; @Component({ - selector: 'tb-complex-version-create', - templateUrl: './complex-version-create.component.html', - styleUrls: ['./version-control.scss'] + selector: 'tb-complex-version-create', + templateUrl: './complex-version-create.component.html', + styleUrls: ['./version-control.scss'], + standalone: false }) export class ComplexVersionCreateComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.ts b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.ts index 4bf029404b..8eab024032 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.ts @@ -35,9 +35,10 @@ import { share } from 'rxjs/operators'; import { parseHttpErrorMessage } from '@core/utils'; @Component({ - selector: 'tb-complex-version-load', - templateUrl: './complex-version-load.component.html', - styleUrls: ['./version-control.scss'] + selector: 'tb-complex-version-load', + templateUrl: './complex-version-load.component.html', + styleUrls: ['./version-control.scss'], + standalone: false }) export class ComplexVersionLoadComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts index 74a5fd9836..de33d70bfc 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts @@ -44,21 +44,22 @@ import { isDefinedAndNotNull } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-entity-types-version-create', - templateUrl: './entity-types-version-create.component.html', - styleUrls: ['./entity-types-version.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityTypesVersionCreateComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => EntityTypesVersionCreateComponent), - multi: true - } - ] + selector: 'tb-entity-types-version-create', + templateUrl: './entity-types-version-create.component.html', + styleUrls: ['./entity-types-version.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityTypesVersionCreateComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityTypesVersionCreateComponent), + multi: true + } + ], + standalone: false }) export class EntityTypesVersionCreateComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts index 254a178091..bdcad871de 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts @@ -44,21 +44,22 @@ import { RemoveOtherEntitiesConfirmComponent } from '@home/components/vc/remove- import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-entity-types-version-load', - templateUrl: './entity-types-version-load.component.html', - styleUrls: ['./entity-types-version.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityTypesVersionLoadComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => EntityTypesVersionLoadComponent), - multi: true - } - ] + selector: 'tb-entity-types-version-load', + templateUrl: './entity-types-version-load.component.html', + styleUrls: ['./entity-types-version.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityTypesVersionLoadComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityTypesVersionLoadComponent), + multi: true + } + ], + standalone: false }) export class EntityTypesVersionLoadComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts index b01134f85c..7dfafba9a2 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts @@ -36,9 +36,10 @@ import { share } from 'rxjs/operators'; import { parseHttpErrorMessage } from '@core/utils'; @Component({ - selector: 'tb-entity-version-create', - templateUrl: './entity-version-create.component.html', - styleUrls: ['./version-control.scss'] + selector: 'tb-entity-version-create', + templateUrl: './entity-version-create.component.html', + styleUrls: ['./version-control.scss'], + standalone: false }) export class EntityVersionCreateComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts index 84480efb35..d7a6cd8003 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts @@ -49,10 +49,11 @@ interface DiffInfo { } @Component({ - selector: 'tb-entity-version-diff', - templateUrl: './entity-version-diff.component.html', - styleUrls: ['./entity-version-diff.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-entity-version-diff', + templateUrl: './entity-version-diff.component.html', + styleUrls: ['./entity-version-diff.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class EntityVersionDiffComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts index c48648dacf..06b5f60441 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts @@ -36,9 +36,10 @@ import { parseHttpErrorMessage } from '@core/utils'; import { EntityType } from '@shared/models/entity-type.models'; @Component({ - selector: 'tb-entity-version-restore', - templateUrl: './entity-version-restore.component.html', - styleUrls: ['./version-control.scss'] + selector: 'tb-entity-version-restore', + templateUrl: './entity-version-restore.component.html', + styleUrls: ['./version-control.scss'], + standalone: false }) export class EntityVersionRestoreComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts index 96f28e2b4a..c50b7d6c36 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts @@ -57,9 +57,10 @@ import { AdminService } from '@core/http/admin.service'; import { FormBuilder } from '@angular/forms'; @Component({ - selector: 'tb-entity-versions-table', - templateUrl: './entity-versions-table.component.html', - styleUrls: ['./entity-versions-table.component.scss'] + selector: 'tb-entity-versions-table', + templateUrl: './entity-versions-table.component.html', + styleUrls: ['./entity-versions-table.component.scss'], + standalone: false }) export class EntityVersionsTableComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.ts b/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.ts index b5590cd841..dd9baf20e4 100644 --- a/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.ts @@ -23,9 +23,10 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @Component({ - selector: 'tb-remove-other-entities-confirm', - templateUrl: './remove-other-entities-confirm.component.html', - styleUrls: [] + selector: 'tb-remove-other-entities-confirm', + templateUrl: './remove-other-entities-confirm.component.html', + styleUrls: [], + standalone: false }) export class RemoveOtherEntitiesConfirmComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts index f4959fa761..88ffbd9171 100644 --- a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts @@ -38,9 +38,10 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-repository-settings', - templateUrl: './repository-settings.component.html', - styleUrls: ['./repository-settings.component.scss', './../../pages/admin/settings-card.scss'] + selector: 'tb-repository-settings', + templateUrl: './repository-settings.component.html', + styleUrls: ['./repository-settings.component.scss', './../../pages/admin/settings-card.scss'], + standalone: false }) export class RepositorySettingsComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/vc/version-control.component.ts b/ui-ngx/src/app/modules/home/components/vc/version-control.component.ts index 4a2c944e2c..f3f4863653 100644 --- a/ui-ngx/src/app/modules/home/components/vc/version-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/version-control.component.ts @@ -26,9 +26,10 @@ import { Observable } from 'rxjs'; import { TbPopoverComponent } from '@shared/components/popover.component'; @Component({ - selector: 'tb-version-control', - templateUrl: './version-control.component.html', - styleUrls: ['./version-control.component.scss'] + selector: 'tb-version-control', + templateUrl: './version-control.component.html', + styleUrls: ['./version-control.component.scss'], + standalone: false }) export class VersionControlComponent implements OnInit, HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts index 367f9c8a6e..8c2def215e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts @@ -37,10 +37,11 @@ export interface ManageWidgetActionsDialogData { } @Component({ - selector: 'tb-manage-widget-actions-dialog', - templateUrl: './manage-widget-actions-dialog.component.html', - providers: [], - styleUrls: [] + selector: 'tb-manage-widget-actions-dialog', + templateUrl: './manage-widget-actions-dialog.component.html', + providers: [], + styleUrls: [], + standalone: false }) export class ManageWidgetActionsDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index 8779085003..2da5066816 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -57,16 +57,17 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ - selector: 'tb-manage-widget-actions', - templateUrl: './manage-widget-actions.component.html', - styleUrls: ['./manage-widget-actions.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ManageWidgetActionsComponent), - multi: true - } - ] + selector: 'tb-manage-widget-actions', + templateUrl: './manage-widget-actions.component.html', + styleUrls: ['./manage-widget-actions.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ManageWidgetActionsComponent), + multi: true + } + ], + standalone: false }) export class ManageWidgetActionsComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index 5549531100..b652f76e06 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -66,10 +66,11 @@ export interface WidgetActionDialogData { } @Component({ - selector: 'tb-widget-action-dialog', - templateUrl: './widget-action-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: WidgetActionDialogComponent}], - styleUrls: [] + selector: 'tb-widget-action-dialog', + templateUrl: './widget-action-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: WidgetActionDialogComponent }], + styleUrls: [], + standalone: false }) export class WidgetActionDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts index 73edce98ee..6d049826d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts @@ -33,9 +33,10 @@ import { } from '@home/components/widget/lib/count/count-widget.models'; @Component({ - selector: 'tb-alarm-count-basic-config', - templateUrl: './alarm-count-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-alarm-count-basic-config', + templateUrl: './alarm-count-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class AlarmCountBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts index a2f25da117..802c1bb0bc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts @@ -29,9 +29,10 @@ import { } from '@home/components/widget/config/timewindow-config-panel.component'; @Component({ - selector: 'tb-alarms-table-basic-config', - templateUrl: './alarms-table-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-alarms-table-basic-config', + templateUrl: './alarms-table-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class AlarmsTableBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.ts index b7edac8efb..93b9d03d73 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.ts @@ -38,9 +38,10 @@ import { } from '@home/components/widget/lib/button/action-button-widget.models'; @Component({ - selector: 'tb-action-button-basic-config', - templateUrl: './action-button-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-action-button-basic-config', + templateUrl: './action-button-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class ActionButtonBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.ts index ab5ef0df91..b909cd0e57 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.ts @@ -29,9 +29,10 @@ import { } from '@home/components/widget/lib/button/command-button-widget.models'; @Component({ - selector: 'tb-command-button-basic-config', - templateUrl: './command-button-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-command-button-basic-config', + templateUrl: './command-button-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class CommandButtonBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts index 871807ec3e..92d808f239 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts @@ -34,9 +34,10 @@ import { import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models'; @Component({ - selector: 'tb-power-button-basic-config', - templateUrl: './power-button-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-power-button-basic-config', + templateUrl: './power-button-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts index 895848a933..532d543719 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts @@ -38,9 +38,10 @@ import { } from '@home/components/widget/lib/button/segmented-button-widget.models'; @Component({ - selector: 'tb-segmented-button-basic-config', - templateUrl: './segmented-button-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-segmented-button-basic-config', + templateUrl: './segmented-button-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class SegmentedButtonBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.ts index 17e6ddf0c9..5e72be0946 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.ts @@ -33,9 +33,10 @@ import { type ButtonAppearanceType = 'checked' | 'unchecked'; @Component({ - selector: 'tb-toggle-button-basic-config', - templateUrl: './toggle-button-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-toggle-button-basic-config', + templateUrl: './toggle-button-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class ToggleButtonBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts index aba164f60e..8751ced932 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts @@ -56,17 +56,18 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { getSourceTbUnitSymbol, TbUnit } from '@shared/models/unit.models'; @Component({ - selector: 'tb-aggregated-data-key-row', - templateUrl: './aggregated-data-key-row.component.html', - styleUrls: ['./aggregated-data-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AggregatedDataKeyRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-aggregated-data-key-row', + templateUrl: './aggregated-data-key-row.component.html', + styleUrls: ['./aggregated-data-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AggregatedDataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts index 2931c6eeee..d2cba50bb8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts @@ -40,17 +40,18 @@ import { aggregatedValueCardDefaultKeySettings } from '@home/components/widget/l import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-aggregated-data-keys-panel', - templateUrl: './aggregated-data-keys-panel.component.html', - styleUrls: ['./aggregated-data-keys-panel.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AggregatedDataKeysPanelComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-aggregated-data-keys-panel', + templateUrl: './aggregated-data-keys-panel.component.html', + styleUrls: ['./aggregated-data-keys-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AggregatedDataKeysPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class AggregatedDataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts index 91666396b9..75c938dd23 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts @@ -52,9 +52,10 @@ import { } from '@shared/models/time/time.models'; @Component({ - selector: 'tb-aggregated-value-card-basic-config', - templateUrl: './aggregated-value-card-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-aggregated-value-card-basic-config', + templateUrl: './aggregated-value-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts index 7c782da2be..59a8f07307 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts @@ -29,9 +29,10 @@ import { } from '@home/components/widget/lib/cards/label-card-widget.models'; @Component({ - selector: 'tb-label-card-basic-config', - templateUrl: './label-card-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-label-card-basic-config', + templateUrl: './label-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class LabelCardBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts index e587269b15..312c8cb01e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts @@ -41,9 +41,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-label-value-card-basic-config', - templateUrl: './label-value-card-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-label-value-card-basic-config', + templateUrl: './label-value-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class LabelValueCardBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts index cb7331721d..6aa3f4196a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts @@ -31,9 +31,10 @@ import { import { badgePositionTranslationsMap } from '@app/shared/models/mobile-app.models'; @Component({ - selector: 'tb-mobile-app-qr-code-basic-config', - templateUrl: './mobile-app-qr-code-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-mobile-app-qr-code-basic-config', + templateUrl: './mobile-app-qr-code-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class MobileAppQrCodeBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts index 1f8347a50e..3e184bae7c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts @@ -46,9 +46,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-progress-bar-basic-config', - templateUrl: './progress-bar-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-progress-bar-basic-config', + templateUrl: './progress-bar-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class ProgressBarBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts index 3594c8dc98..4e05996186 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts @@ -37,9 +37,10 @@ import { isUndefined } from '@core/utils'; import { getLabel, setLabel } from '@shared/models/widget-settings.models'; @Component({ - selector: 'tb-simple-card-basic-config', - templateUrl: './simple-card-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-simple-card-basic-config', + templateUrl: './simple-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts index 24434ea1d6..c7533f6932 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts @@ -30,9 +30,10 @@ import { } from '@home/components/widget/config/timewindow-config-panel.component'; @Component({ - selector: 'tb-timeseries-table-basic-config', - templateUrl: './timeseries-table-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-timeseries-table-basic-config', + templateUrl: './timeseries-table-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class TimeseriesTableBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts index 7b6347c9a5..a75a84fcca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts @@ -30,9 +30,10 @@ import { } from '@home/components/widget/lib/cards/unread-notification-widget.models'; @Component({ - selector: 'tb-unread-notification-basic-config', - templateUrl: './unread-notification-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-unread-notification-basic-config', + templateUrl: './unread-notification-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts index ef3fbabbb0..2c210b90f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts @@ -46,9 +46,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-value-card-basic-config', - templateUrl: './value-card-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-value-card-basic-config', + templateUrl: './value-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.ts index d48266e72d..acbce131d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.ts @@ -39,9 +39,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-value-chart-card-basic-config', - templateUrl: './value-chart-card-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-value-chart-card-basic-config', + templateUrl: './value-chart-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class ValueChartCardBasicConfigComponent extends BasicWidgetConfigComponent { 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 dccbb62f7d..99aed7565a 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 @@ -31,9 +31,10 @@ import { } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; @Component({ - selector: 'tb-bar-chart-basic-config', - templateUrl: './latest-chart-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-bar-chart-basic-config', + templateUrl: './latest-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts index 1436275c7d..9204a901b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts @@ -49,9 +49,10 @@ import { TimeSeriesChartType } from '@home/components/widget/lib/chart/time-seri import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-bar-chart-with-labels-basic-config', - templateUrl: './bar-chart-with-labels-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-bar-chart-with-labels-basic-config', + templateUrl: './bar-chart-with-labels-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts index e7ff98eed8..add7085af1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts @@ -32,17 +32,18 @@ import { deepClone } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-comparison-key-row', - templateUrl: './comparison-key-row.component.html', - styleUrls: ['./comparison-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ComparisonKeyRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-comparison-key-row', + templateUrl: './comparison-key-row.component.html', + styleUrls: ['./comparison-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ComparisonKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ComparisonKeyRowComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts index 162f55ae9e..2409c9af57 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts @@ -27,17 +27,18 @@ import { DataKey, DatasourceType } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-comparison-keys-table', - templateUrl: './comparison-keys-table.component.html', - styleUrls: ['./comparison-keys-table.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ComparisonKeysTableComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-comparison-keys-table', + templateUrl: './comparison-keys-table.component.html', + styleUrls: ['./comparison-keys-table.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ComparisonKeysTableComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ComparisonKeysTableComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts index c9d0af5dfd..ebd5fb8d69 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts @@ -32,9 +32,10 @@ import { } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; @Component({ - selector: 'tb-doughnut-basic-config', - templateUrl: './latest-chart-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-doughnut-basic-config', + templateUrl: './latest-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class DoughnutBasicConfigComponent extends LatestChartBasicConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts index f674487500..bb65fb2e27 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts @@ -30,9 +30,10 @@ import { } from '@home/components/widget/config/timewindow-config-panel.component'; @Component({ - selector: 'tb-flot-basic-config', - templateUrl: './flot-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-flot-basic-config', + templateUrl: './flot-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class FlotBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts index 1f18fbb3a8..741a25638d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts @@ -31,9 +31,10 @@ import { } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; @Component({ - selector: 'tb-pie-chart-basic-config', - templateUrl: './latest-chart-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-pie-chart-basic-config', + templateUrl: './latest-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class PieChartBasicConfigComponent extends LatestChartBasicConfigComponent { 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 3050b0550d..59864ce5e3 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 @@ -31,9 +31,10 @@ import { } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; @Component({ - selector: 'tb-polar-area-chart-basic-config', - templateUrl: './latest-chart-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-polar-area-chart-basic-config', + templateUrl: './latest-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/radar-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/radar-chart-basic-config.component.ts index 6af8b7de34..22656ca16a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/radar-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/radar-chart-basic-config.component.ts @@ -31,9 +31,10 @@ import { } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; @Component({ - selector: 'tb-radar-chart-basic-config', - templateUrl: './latest-chart-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-radar-chart-basic-config', + templateUrl: './latest-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class RadarChartBasicConfigComponent extends LatestChartBasicConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts index d93a77e879..0527045650 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts @@ -60,9 +60,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-range-chart-basic-config', - templateUrl: './range-chart-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-range-chart-basic-config', + templateUrl: './range-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 0d16ca6bee..5b1410e5d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -56,9 +56,10 @@ import { TimeSeriesChartTooltipTrigger } from '@home/components/widget/lib/chart import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-time-series-chart-basic-config', - templateUrl: './time-series-chart-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-time-series-chart-basic-config', + templateUrl: './time-series-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts index bca32ef235..e84587c758 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts @@ -76,17 +76,18 @@ export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors }; @Component({ - selector: 'tb-data-key-row', - templateUrl: './data-key-row.component.html', - styleUrls: ['./data-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataKeyRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-data-key-row', + templateUrl: './data-key-row.component.html', + styleUrls: ['./data-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts index d0b6ca68d9..a42b046bb8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts @@ -50,22 +50,23 @@ import { FormProperty } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-data-keys-panel', - templateUrl: './data-keys-panel.component.html', - styleUrls: ['./data-keys-panel.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataKeysPanelComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DataKeysPanelComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-data-keys-panel', + templateUrl: './data-keys-panel.component.html', + styleUrls: ['./data-keys-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeysPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataKeysPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts index f63825a285..0320dc7fd1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts @@ -28,16 +28,17 @@ import { MatDialog } from '@angular/material/dialog'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-widget-actions-panel', - templateUrl: './widget-actions-panel.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetActionsPanelComponent), - multi: true - } - ] + selector: 'tb-widget-actions-panel', + templateUrl: './widget-actions-panel.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetActionsPanelComponent), + multi: true + } + ], + standalone: false }) export class WidgetActionsPanelComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.ts index 1a718e5895..7b6c1df610 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.ts @@ -36,9 +36,10 @@ import { } from '@home/components/widget/config/timewindow-config-panel.component'; @Component({ - selector: 'tb-entities-table-basic-config', - templateUrl: './entities-table-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-entities-table-basic-config', + templateUrl: './entities-table-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.ts index b7f6ca68ee..7f23351cf6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entity-count-basic-config.component.ts @@ -28,9 +28,10 @@ import { UtilsService } from '@core/services/utils.service'; import { countDefaultSettings, CountWidgetSettings } from '@home/components/widget/lib/count/count-widget.models'; @Component({ - selector: 'tb-entity-count-basic-config', - templateUrl: './entity-count-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-entity-count-basic-config', + templateUrl: './entity-count-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class EntityCountBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.ts index f0b8188075..04c3141aef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.ts @@ -33,9 +33,10 @@ import { import { isUndefined } from '@core/utils'; @Component({ - selector: 'tb-compass-gauge-basic-config', - templateUrl: './compass-gauge-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-compass-gauge-basic-config', + templateUrl: './compass-gauge-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class CompassGaugeBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts index c90c19aca8..e0ca65cdea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts @@ -45,9 +45,10 @@ import { ColorSettings, ColorType } from '@shared/models/widget-settings.models' import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-digital-simple-gauge-basic-config', - templateUrl: './digital-simple-gauge-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-digital-simple-gauge-basic-config', + templateUrl: './digital-simple-gauge-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/radial-gauge-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/radial-gauge-basic-config.component.ts index 4b832956e6..e156c0bbfc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/radial-gauge-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/radial-gauge-basic-config.component.ts @@ -25,9 +25,10 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com import { WidgetConfigComponentData } from '@home/models/widget-component.models'; @Component({ - selector: 'tb-radial-gauge-basic-config', - templateUrl: './analog-gauge-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-radial-gauge-basic-config', + templateUrl: './analog-gauge-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class RadialGaugeBasicConfigComponent extends GaugeBasicConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/thermometer-scale-gauge-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/thermometer-scale-gauge-basic-config.component.ts index c8d560370e..d173b9bce9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/thermometer-scale-gauge-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/thermometer-scale-gauge-basic-config.component.ts @@ -25,9 +25,10 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com import { WidgetConfigComponentData } from '@home/models/widget-component.models'; @Component({ - selector: 'tb-thermometer-scale-gauge-basic-config', - templateUrl: './analog-gauge-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-thermometer-scale-gauge-basic-config', + templateUrl: './analog-gauge-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class ThermometerScaleGaugeBasicConfigComponent extends GaugeBasicConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts index f83f7cee34..1ac5875abf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts @@ -47,9 +47,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-battery-level-basic-config', - templateUrl: './battery-level-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-battery-level-basic-config', + templateUrl: './battery-level-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class BatteryLevelBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts index c232d966e2..fb63143c98 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts @@ -67,9 +67,10 @@ import { UtilsService } from '@core/services/utils.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-liquid-level-card-basic-config', - templateUrl: './liquid-level-card-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-liquid-level-card-basic-config', + templateUrl: './liquid-level-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.ts index 20ff2d8a0c..f1d3b99969 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.ts @@ -50,9 +50,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-signal-strength-basic-config', - templateUrl: './signal-strength-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-signal-strength-basic-config', + templateUrl: './signal-strength-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class SignalStrengthBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts index 8abefc479f..3048f509ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts @@ -33,9 +33,10 @@ import { } from '@home/components/widget/lib/indicator/status-widget.models'; @Component({ - selector: 'tb-status-widget-basic-config', - templateUrl: './status-widget-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-status-widget-basic-config', + templateUrl: './status-widget-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class StatusWidgetBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts index 82f45dfe1d..0c3e6798c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.ts @@ -32,9 +32,10 @@ import { import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition'; @Component({ - selector: 'tb-map-basic-config', - templateUrl: './map-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-map-basic-config', + templateUrl: './map-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class MapBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts index f464cdb66c..c45fd8bb05 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts @@ -33,9 +33,10 @@ import { import { ValueType } from '@shared/models/constants'; @Component({ - selector: 'tb-single-switch-basic-config', - templateUrl: './single-switch-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-single-switch-basic-config', + templateUrl: './single-switch-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class SingleSwitchBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts index ccd8ca835a..481936aee3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts @@ -36,9 +36,10 @@ import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-slider-basic-config', - templateUrl: './slider-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-slider-basic-config', + templateUrl: './slider-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class SliderBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts index a149118743..d9806967d3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts @@ -36,9 +36,10 @@ import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; type ButtonAppearanceType = 'left' | 'right'; @Component({ - selector: 'tb-value-stepper-basic-config', - templateUrl: './value-stepper-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-value-stepper-basic-config', + templateUrl: './value-stepper-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class ValueStepperBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.ts index f1da790f3f..7f31ab4363 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.ts @@ -30,9 +30,10 @@ import { isUndefined } from '@core/utils'; import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models'; @Component({ - selector: 'tb-scada-symbol-basic-config', - templateUrl: './scada-symbol-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-scada-symbol-basic-config', + templateUrl: './scada-symbol-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class ScadaSymbolBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.ts index 003ccef095..3b8c86fe5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.ts @@ -47,9 +47,10 @@ import { import { getSourceTbUnitSymbol, TbUnit } from '@shared/models/unit.models'; @Component({ - selector: 'tb-wind-speed-direction-basic-config', - templateUrl: './wind-speed-direction-basic-config.component.html', - styleUrls: ['../basic-config.scss'] + selector: 'tb-wind-speed-direction-basic-config', + templateUrl: './wind-speed-direction-basic-config.component.html', + styleUrls: ['../basic-config.scss'], + standalone: false }) export class WindSpeedDirectionBasicConfigComponent extends BasicWidgetConfigComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts index dcec6497da..6dcfee5146 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts @@ -47,21 +47,22 @@ import { FormProperty } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-datasource', - templateUrl: './datasource.component.html', - styleUrls: ['./datasource.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DatasourceComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DatasourceComponent), - multi: true, - } - ] + selector: 'tb-datasource', + templateUrl: './datasource.component.html', + styleUrls: ['./datasource.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatasourceComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DatasourceComponent), + multi: true, + } + ], + standalone: false }) export class DatasourceComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts index dd518177d0..36dcbbd9f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts @@ -46,21 +46,22 @@ import { FormProperty } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-datasources', - templateUrl: './datasources.component.html', - styleUrls: ['./datasources.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DatasourcesComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DatasourcesComponent), - multi: true, - } - ] + selector: 'tb-datasources', + templateUrl: './datasources.component.html', + styleUrls: ['./datasources.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatasourcesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DatasourcesComponent), + multi: true, + } + ], + standalone: false }) export class DatasourcesComponent implements ControlValueAccessor, OnInit, Validator, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts index 3b68b52668..bcabcf9099 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts @@ -35,21 +35,22 @@ import { EntityAliasSelectCallbacks } from '@home/components/widget/lib/settings import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-target-device', - templateUrl: './target-device.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TargetDeviceComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TargetDeviceComponent), - multi: true, - } - ] + selector: 'tb-target-device', + templateUrl: './target-device.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TargetDeviceComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TargetDeviceComponent), + multi: true, + } + ], + standalone: false }) export class TargetDeviceComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts index 1c1ecf9e2f..03a7804658 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts @@ -49,16 +49,17 @@ export const setTimewindowConfig = (config: WidgetConfig, data: TimewindowConfig }; @Component({ - selector: 'tb-timewindow-config-panel', - templateUrl: './timewindow-config-panel.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimewindowConfigPanelComponent), - multi: true - } - ] + selector: 'tb-timewindow-config-panel', + templateUrl: './timewindow-config-panel.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimewindowConfigPanelComponent), + multi: true + } + ], + standalone: false }) export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts index 0a3dc78902..4370dfc358 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts @@ -26,11 +26,12 @@ import { deepClone } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-timewindow-style-panel', - templateUrl: './timewindow-style-panel.component.html', - providers: [], - styleUrls: ['./timewindow-style-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-timewindow-style-panel', + templateUrl: './timewindow-style-panel.component.html', + providers: [], + styleUrls: ['./timewindow-style-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimewindowStylePanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts index 03abbf7c2a..d0e0eef7e2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts @@ -23,16 +23,17 @@ import { Timewindow } from '@shared/models/time/time.models'; import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component'; @Component({ - selector: 'tb-timewindow-style', - templateUrl: './timewindow-style.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimewindowStyleComponent), - multi: true - } - ] + selector: 'tb-timewindow-style', + templateUrl: './timewindow-style.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimewindowStyleComponent), + multi: true + } + ], + standalone: false }) export class TimewindowStyleComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts index 3c05469d04..31c63187ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts @@ -44,8 +44,9 @@ export interface CustomDialogContainerData { } @Component({ - selector: 'tb-custom-dialog-container-component', - template: '' + selector: 'tb-custom-dialog-container-component', + template: '', + standalone: false }) export class CustomDialogContainerComponent extends DialogComponent implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.ts index 667316f76b..5b791ada3f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.ts @@ -34,9 +34,10 @@ export interface EmbedDashboardDialogData { } @Component({ - selector: 'tb-embed-dashboard-dialog', - templateUrl: './embed-dashboard-dialog.component.html', - styleUrls: ['./embed-dashboard-dialog.component.scss'] + selector: 'tb-embed-dashboard-dialog', + templateUrl: './embed-dashboard-dialog.component.html', + styleUrls: ['./embed-dashboard-dialog.component.scss'], + standalone: false }) export class EmbedDashboardDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index fa5402f808..06e0cb73ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -158,9 +158,10 @@ interface AlarmWidgetActionDescriptor extends TableCellButtonActionDescriptor { } @Component({ - selector: 'tb-alarms-table-widget', - templateUrl: './alarms-table-widget.component.html', - styleUrls: ['./alarms-table-widget.component.scss', './../table-widget.scss'] + selector: 'tb-alarms-table-widget', + templateUrl: './alarms-table-widget.component.html', + styleUrls: ['./alarms-table-widget.component.scss', './../table-widget.scss'], + standalone: false }) export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.ts index 9a397eaf9b..eaa723acd3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.ts @@ -26,10 +26,11 @@ import { import { WidgetButtonAppearance } from '@shared/components/button/widget-button.models'; @Component({ - selector: 'tb-action-button-widget', - templateUrl: './action-button-widget.component.html', - styleUrls: ['../action/action-widget.scss', './action-button-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-action-button-widget', + templateUrl: './action-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './action-button-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ActionButtonWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.ts index ca863f9108..70298808da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.ts @@ -26,10 +26,11 @@ import { } from '@home/components/widget/lib/button/command-button-widget.models'; @Component({ - selector: 'tb-command-button-widget', - templateUrl: './command-button-widget.component.html', - styleUrls: ['../action/action-widget.scss', './command-button-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-command-button-widget', + templateUrl: './command-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './command-button-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class CommandButtonWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.ts index 9c20a279ed..a1601a6fb8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.ts @@ -28,10 +28,11 @@ import { Observable } from 'rxjs'; import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; @Component({ - selector: 'tb-toggle-button-widget', - templateUrl: './toggle-button-widget.component.html', - styleUrls: ['../action/action-widget.scss', './toggle-button-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-toggle-button-widget', + templateUrl: './toggle-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './toggle-button-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ToggleButtonWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts index efff97c29c..46382dab77 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts @@ -28,10 +28,11 @@ import { ComponentStyle } from '@shared/models/widget-settings.models'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; @Component({ - selector: 'tb-two-segment-button-widget', - templateUrl: './two-segment-button-widget.component.html', - styleUrls: ['../action/action-widget.scss', './two-segment-button-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-two-segment-button-widget', + templateUrl: './two-segment-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './two-segment-button-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TwoSegmentButtonWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts index 3a4a836c5e..c4b07619e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts @@ -63,9 +63,10 @@ const valuesLayoutHeight = 66; const valuesLayoutVerticalPadding = 16; @Component({ - selector: 'tb-aggregated-value-card-widget', - templateUrl: './aggregated-value-card-widget.component.html', - styleUrls: ['./aggregated-value-card-widget.component.scss'] + selector: 'tb-aggregated-value-card-widget', + templateUrl: './aggregated-value-card-widget.component.html', + styleUrls: ['./aggregated-value-card-widget.component.scss'], + standalone: false }) export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts index 26c3901e4b..b41f5ed02a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts @@ -32,10 +32,11 @@ import { } from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; @Component({ - selector: 'tb-api-usage-widget', - templateUrl: './api-usage-widget.component.html', - styleUrls: ['api-usage-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-api-usage-widget', + templateUrl: './api-usage-widget.component.html', + styleUrls: ['api-usage-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ApiUsageWidgetComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts index 195e683c57..52905a0469 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts @@ -45,10 +45,11 @@ import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ - selector: 'tb-label-card-widget', - templateUrl: './label-card-widget.component.html', - styleUrls: ['./label-card-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-label-card-widget', + templateUrl: './label-card-widget.component.html', + styleUrls: ['./label-card-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class LabelCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts index 978d52d854..04e2c03679 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts @@ -50,10 +50,11 @@ import { import { isDefinedAndNotNull } from '@core/utils'; @Component({ - selector: 'tb-label-value-card-widget', - templateUrl: './label-value-card-widget.component.html', - styleUrls: ['./label-value-card-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-label-value-card-widget', + templateUrl: './label-value-card-widget.component.html', + styleUrls: ['./label-value-card-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class LabelValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.ts index 30976c8442..a39044c759 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.ts @@ -32,9 +32,10 @@ export interface NotificationTypeFilterPanelData { } @Component({ - selector: 'tb-notification-type-filter-panel', - templateUrl: './notification-type-filter-panel.component.html', - styleUrls: ['notification-type-filter-panel.component.scss'] + selector: 'tb-notification-type-filter-panel', + templateUrl: './notification-type-filter-panel.component.html', + styleUrls: ['notification-type-filter-panel.component.scss'], + standalone: false }) export class NotificationTypeFilterPanelComponent implements OnInit{ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts index 66f5d90254..bc4180b0e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts @@ -52,10 +52,11 @@ const defaultAspect = defaultLayoutHeight / 150; const simplifiedAspect = simplifiedLayoutHeight / 150; @Component({ - selector: 'tb-progress-bar-widget', - templateUrl: './progress-bar-widget.component.html', - styleUrls: ['./progress-bar-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-progress-bar-widget', + templateUrl: './progress-bar-widget.component.html', + styleUrls: ['./progress-bar-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ProgressBarWidgetComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts index 450d59e22e..e68cddacf0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts @@ -54,10 +54,11 @@ import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-unread-notification-widget', - templateUrl: './unread-notification-widget.component.html', - styleUrls: ['unread-notification-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-unread-notification-widget', + templateUrl: './unread-notification-widget.component.html', + styleUrls: ['unread-notification-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts index 4db8f11ba2..b994ff8b23 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts @@ -52,9 +52,10 @@ const squareLayoutSize = 160; const horizontalLayoutHeight = 80; @Component({ - selector: 'tb-value-card-widget', - templateUrl: './value-card-widget.component.html', - styleUrls: ['./value-card-widget.component.scss'] + selector: 'tb-value-card-widget', + templateUrl: './value-card-widget.component.html', + styleUrls: ['./value-card-widget.component.scss'], + standalone: false }) export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts index 0198fdf6e3..5da48a00d3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts @@ -62,10 +62,11 @@ const layoutHeight = 56; const valueRelativeWidth = 0.35; @Component({ - selector: 'tb-value-chart-card-widget', - templateUrl: './value-chart-card-widget.component.html', - styleUrls: ['./value-chart-card-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-value-chart-card-widget', + templateUrl: './value-chart-card-widget.component.html', + styleUrls: ['./value-chart-card-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.component.ts index b6c88a7d3a..3fb90f4f21 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.component.ts @@ -30,10 +30,11 @@ import { import { TbBarsChart } from '@home/components/widget/lib/chart/bars-chart'; @Component({ - selector: 'tb-bar-chart-widget', - templateUrl: './latest-chart-widget.component.html', - styleUrls: [], - encapsulation: ViewEncapsulation.None + selector: 'tb-bar-chart-widget', + templateUrl: './latest-chart-widget.component.html', + styleUrls: [], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class BarChartWidgetComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index f9bd213fdb..1f63cd8638 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -43,10 +43,11 @@ import { DataKey } from '@shared/models/widget.models'; import { WidgetComponent } from '@home/components/widget/widget.component'; @Component({ - selector: 'tb-bar-chart-with-labels-widget', - templateUrl: './bar-chart-with-labels-widget.component.html', - styleUrls: ['./bar-chart-with-labels-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-bar-chart-with-labels-widget', + templateUrl: './bar-chart-with-labels-widget.component.html', + styleUrls: ['./bar-chart-with-labels-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts index 00cc814246..64b4f2caf2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts @@ -31,10 +31,11 @@ import { } from '@home/components/widget/lib/chart/latest-chart.component'; @Component({ - selector: 'tb-doughnut-widget', - templateUrl: './latest-chart-widget.component.html', - styleUrls: [], - encapsulation: ViewEncapsulation.None + selector: 'tb-doughnut-widget', + templateUrl: './latest-chart-widget.component.html', + styleUrls: [], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DoughnutWidgetComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts index 634c6f5a1e..b6576f8a3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts @@ -49,10 +49,11 @@ export interface LatestChartComponentCallbacks { } @Component({ - selector: 'tb-latest-chart', - templateUrl: './latest-chart.component.html', - styleUrls: ['./latest-chart.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-latest-chart', + templateUrl: './latest-chart.component.html', + styleUrls: ['./latest-chart.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts index 2ea605739c..3da64540e3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts @@ -30,10 +30,11 @@ import { } from '@home/components/widget/lib/chart/pie-chart-widget.models'; @Component({ - selector: 'tb-pie-chart-widget', - templateUrl: './latest-chart-widget.component.html', - styleUrls: [], - encapsulation: ViewEncapsulation.None + selector: 'tb-pie-chart-widget', + templateUrl: './latest-chart-widget.component.html', + styleUrls: [], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class PieChartWidgetComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/polar-area-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/polar-area-widget.component.ts index adc74628c5..d54ced555b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/polar-area-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/polar-area-widget.component.ts @@ -30,10 +30,11 @@ import { } from '@home/components/widget/lib/chart/polar-area-widget.models'; @Component({ - selector: 'tb-polar-area-chart-widget', - templateUrl: './latest-chart-widget.component.html', - styleUrls: [], - encapsulation: ViewEncapsulation.None + selector: 'tb-polar-area-chart-widget', + templateUrl: './latest-chart-widget.component.html', + styleUrls: [], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class PolarAreaWidgetComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/radar-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/radar-chart-widget.component.ts index 305ae940ca..7bf770e3ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/radar-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/radar-chart-widget.component.ts @@ -30,10 +30,11 @@ import { import { TbRadarChart } from '@home/components/widget/lib/chart/radar-chart'; @Component({ - selector: 'tb-radar-chart-widget', - templateUrl: './latest-chart-widget.component.html', - styleUrls: [], - encapsulation: ViewEncapsulation.None + selector: 'tb-radar-chart-widget', + templateUrl: './latest-chart-widget.component.html', + styleUrls: [], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class RadarChartWidgetComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 27fe86a393..2412d51a83 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -54,10 +54,11 @@ import { TbUnit } from '@shared/models/unit.models'; import { UnitService } from '@core/services/unit.service'; @Component({ - selector: 'tb-range-chart-widget', - templateUrl: './range-chart-widget.component.html', - styleUrls: ['./range-chart-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-range-chart-widget', + templateUrl: './range-chart-widget.component.html', + styleUrls: ['./range-chart-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts index b96ff3996f..aa94ee717c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts @@ -46,10 +46,11 @@ import { mergeDeep } from '@core/utils'; import { WidgetComponent } from '@home/components/widget/widget.component'; @Component({ - selector: 'tb-time-series-chart-widget', - templateUrl: './time-series-chart-widget.component.html', - styleUrls: ['./time-series-chart-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-time-series-chart-widget', + templateUrl: './time-series-chart-widget.component.html', + styleUrls: ['./time-series-chart-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts index 611223c5df..01a652b03f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts @@ -48,9 +48,10 @@ const layoutHeightWithTitle = 60; const layoutPadding = 24; @Component({ - selector: 'tb-count-widget', - templateUrl: './count-widget.component.html', - styleUrls: ['./count-widget.component.scss'] + selector: 'tb-count-widget', + templateUrl: './count-widget.component.html', + styleUrls: ['./count-widget.component.scss'], + standalone: false }) export class CountWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts index 2870287548..2dc78059e9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts @@ -50,9 +50,10 @@ import { HistoryWindowType, TimewindowType } from '@shared/models/time/time.mode import { isDefined } from '@core/utils'; @Component({ - selector: 'tb-date-range-navigator-widget', - templateUrl: './date-range-navigator.component.html', - styleUrls: ['./date-range-navigator.component.scss'] + selector: 'tb-date-range-navigator-widget', + templateUrl: './date-range-navigator.component.html', + styleUrls: ['./date-range-navigator.component.scss'], + standalone: false }) export class DateRangeNavigatorWidgetComponent extends PageComponent implements OnInit, OnDestroy { @@ -276,10 +277,11 @@ export interface DateRangeNavigatorPanelData { } @Component({ - selector: 'tb-date-range-navigator-panel', - templateUrl: './date-range-navigator-panel.component.html', - styleUrls: ['./date-range-navigator-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-date-range-navigator-panel', + templateUrl: './date-range-navigator-panel.component.html', + styleUrls: ['./date-range-navigator-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DateRangeNavigatorPanelComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts index d9e94ee7bd..d0b07e5359 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts @@ -27,9 +27,10 @@ export interface DisplayColumnsPanelData { } @Component({ - selector: 'tb-display-columns-panel', - templateUrl: './display-columns-panel.component.html', - styleUrls: ['./display-columns-panel.component.scss'] + selector: 'tb-display-columns-panel', + templateUrl: './display-columns-panel.component.html', + styleUrls: ['./display-columns-panel.component.scss'], + standalone: false }) export class DisplayColumnsPanelComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts index 797ea4a03d..5f11d57cc4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -48,9 +48,10 @@ interface EdgesOverviewWidgetSettings { } @Component({ - selector: 'tb-edges-overview-widget', - templateUrl: './edges-overview-widget.component.html', - styleUrls: ['./edges-overview-widget.component.scss'] + selector: 'tb-edges-overview-widget', + templateUrl: './edges-overview-widget.component.html', + styleUrls: ['./edges-overview-widget.component.scss'], + standalone: false }) export class EdgesOverviewWidgetComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-hierarchy-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-hierarchy-widget.component.ts index 5820625076..12cd55f5b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-hierarchy-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-hierarchy-widget.component.ts @@ -72,9 +72,10 @@ import { FormBuilder } from '@angular/forms'; import { Subject } from 'rxjs'; @Component({ - selector: 'tb-entities-hierarchy-widget', - templateUrl: './entities-hierarchy-widget.component.html', - styleUrls: ['./entities-hierarchy-widget.component.scss'] + selector: 'tb-entities-hierarchy-widget', + templateUrl: './entities-hierarchy-widget.component.html', + styleUrls: ['./entities-hierarchy-widget.component.scss'], + standalone: false }) export class EntitiesHierarchyWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index 6c362fa604..cd43dbdf19 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts @@ -123,9 +123,10 @@ interface EntitiesTableWidgetSettings extends TableWidgetSettings { } @Component({ - selector: 'tb-entities-table-widget', - templateUrl: './entities-table-widget.component.html', - styleUrls: ['./entities-table-widget.component.scss', './../table-widget.scss'] + selector: 'tb-entities-table-widget', + templateUrl: './entities-table-widget.component.html', + styleUrls: ['./entities-table-widget.component.scss', './../table-widget.scss'], + standalone: false }) export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts index 0b670879ac..93c93f9004 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts @@ -28,9 +28,10 @@ import { import { isDefinedAndNotNull } from '@core/utils'; @Component({ - selector: 'tb-flot-widget', - templateUrl: './flot-widget.component.html', - styleUrls: [] + selector: 'tb-flot-widget', + templateUrl: './flot-widget.component.html', + styleUrls: [], + standalone: false }) export class FlotWidgetComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts index 447508e808..cef15ed649 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.ts @@ -25,9 +25,10 @@ import { Router } from '@angular/router'; import { DocumentationLink } from '@shared/models/user-settings.models'; @Component({ - selector: 'tb-add-doc-link-dialog', - templateUrl: './add-doc-link-dialog.component.html', - styleUrls: ['./add-doc-link-dialog.component.scss'] + selector: 'tb-add-doc-link-dialog', + templateUrl: './add-doc-link-dialog.component.html', + styleUrls: ['./add-doc-link-dialog.component.scss'], + standalone: false }) export class AddDocLinkDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.ts index f7cac6cdf9..69087d21ee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.ts @@ -24,9 +24,10 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; @Component({ - selector: 'tb-add-quick-link-dialog', - templateUrl: './add-quick-link-dialog.component.html', - styleUrls: ['./add-quick-link-dialog.component.scss'] + selector: 'tb-add-quick-link-dialog', + templateUrl: './add-quick-link-dialog.component.html', + styleUrls: ['./add-quick-link-dialog.component.scss'], + standalone: false }) export class AddQuickLinkDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts index 927c80da30..625ddca093 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts @@ -43,9 +43,10 @@ export interface SystemInfoData { } @Component({ - selector: 'tb-cluster-info-table', - templateUrl: './cluster-info-table.component.html', - styleUrls: ['./cluster-info-table.component.scss'] + selector: 'tb-cluster-info-table', + templateUrl: './cluster-info-table.component.html', + styleUrls: ['./cluster-info-table.component.scss'], + standalone: false }) export class ClusterInfoTableComponent extends PageComponent implements OnInit, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts index 835df2f99d..06808d75ba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts @@ -28,9 +28,10 @@ import { TranslateService } from '@ngx-translate/core'; import { MediaBreakpoints } from '@shared/models/constants'; @Component({ - selector: 'tb-configured-features', - templateUrl: './configured-features.component.html', - styleUrls: ['./home-page-widget.scss', './configured-features.component.scss'] + selector: 'tb-configured-features', + templateUrl: './configured-features.component.html', + styleUrls: ['./home-page-widget.scss', './configured-features.component.scss'], + standalone: false }) export class ConfiguredFeaturesComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts index 73c91d64b4..17f96f3b74 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts @@ -29,17 +29,18 @@ import { DocumentationLink } from '@shared/models/user-settings.models'; import { ErrorStateMatcher } from '@angular/material/core'; @Component({ - selector: 'tb-doc-link', - templateUrl: './doc-link.component.html', - styleUrls: ['./link.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DocLinkComponent), - multi: true - }, - {provide: ErrorStateMatcher, useExisting: DocLinkComponent} - ] + selector: 'tb-doc-link', + templateUrl: './doc-link.component.html', + styleUrls: ['./link.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DocLinkComponent), + multi: true + }, + { provide: ErrorStateMatcher, useExisting: DocLinkComponent } + ], + standalone: false }) export class DocLinkComponent extends PageComponent implements OnInit, ControlValueAccessor, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts index 56b2418a81..74159f184c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts @@ -100,9 +100,10 @@ interface DocLinksWidgetSettings { } @Component({ - selector: 'tb-doc-links-widget', - templateUrl: './doc-links-widget.component.html', - styleUrls: ['./home-page-widget.scss', './links-widget.component.scss'] + selector: 'tb-doc-links-widget', + templateUrl: './doc-links-widget.component.html', + styleUrls: ['./home-page-widget.scss', './links-widget.component.scss'], + standalone: false }) export class DocLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.ts index 224d963a23..6e33e8e645 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.ts @@ -32,9 +32,10 @@ export interface EditLinksDialogData { } @Component({ - selector: 'tb-edit-links-dialog', - templateUrl: './edit-links-dialog.component.html', - styleUrls: ['./edit-links-dialog.component.scss'] + selector: 'tb-edit-links-dialog', + templateUrl: './edit-links-dialog.component.html', + styleUrls: ['./edit-links-dialog.component.scss'], + standalone: false }) export class EditLinksDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts index 13356e6a1c..9d21b73dd3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-completed-dialog.component.ts @@ -23,10 +23,11 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; @Component({ - selector: 'tb-getting-started-completed-dialog', - templateUrl: './getting-started-completed-dialog.component.html', - styleUrls: ['./getting-started-completed-dialog.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-getting-started-completed-dialog', + templateUrl: './getting-started-completed-dialog.component.html', + styleUrls: ['./getting-started-completed-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class GettingStartedCompletedDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts index a6d901c2af..6c13054b67 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts @@ -33,9 +33,10 @@ import { first } from 'rxjs/operators'; import { Authority } from '@shared/models/authority.enum'; @Component({ - selector: 'tb-getting-started-widget', - templateUrl: './getting-started-widget.component.html', - styleUrls: ['./getting-started-widget.component.scss'] + selector: 'tb-getting-started-widget', + templateUrl: './getting-started-widget.component.html', + styleUrls: ['./getting-started-widget.component.scss'], + standalone: false }) export class GettingStartedWidgetComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts index c7d9a640d2..f9221dfb5f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts @@ -52,17 +52,18 @@ import { deepClone } from '@core/utils'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; @Component({ - selector: 'tb-quick-link', - templateUrl: './quick-link.component.html', - styleUrls: ['./link.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => QuickLinkComponent), - multi: true - }, - {provide: ErrorStateMatcher, useExisting: QuickLinkComponent} - ] + selector: 'tb-quick-link', + templateUrl: './quick-link.component.html', + styleUrls: ['./link.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => QuickLinkComponent), + multi: true + }, + { provide: ErrorStateMatcher, useExisting: QuickLinkComponent } + ], + standalone: false }) export class QuickLinkComponent extends PageComponent implements OnInit, ControlValueAccessor, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts index 3c1452decd..7d4783a3fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts @@ -54,9 +54,10 @@ interface QuickLinksWidgetSettings { } @Component({ - selector: 'tb-quick-links-widget', - templateUrl: './quick-links-widget.component.html', - styleUrls: ['./home-page-widget.scss', './links-widget.component.scss'] + selector: 'tb-quick-links-widget', + templateUrl: './quick-links-widget.component.html', + styleUrls: ['./home-page-widget.scss', './links-widget.component.scss'], + standalone: false }) export class QuickLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts index f66c226379..899ffa5aea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts @@ -50,9 +50,10 @@ import { DashboardInfo } from '@shared/models/dashboard.models'; import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; @Component({ - selector: 'tb-recent-dashboards-widget', - templateUrl: './recent-dashboards-widget.component.html', - styleUrls: ['./home-page-widget.scss', './recent-dashboards-widget.component.scss'] + selector: 'tb-recent-dashboards-widget', + templateUrl: './recent-dashboards-widget.component.html', + styleUrls: ['./home-page-widget.scss', './recent-dashboards-widget.component.scss'], + standalone: false }) export class RecentDashboardsWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts index aa7319f495..230d7842c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts @@ -27,9 +27,10 @@ import { UsageInfoService } from '@core/http/usage-info.service'; import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; @Component({ - selector: 'tb-usage-info-widget', - templateUrl: './usage-info-widget.component.html', - styleUrls: ['./home-page-widget.scss', './usage-info-widget.component.scss'] + selector: 'tb-usage-info-widget', + templateUrl: './usage-info-widget.component.html', + styleUrls: ['./home-page-widget.scss', './usage-info-widget.component.scss'], + standalone: false }) export class UsageInfoWidgetComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts index 5d9fc59c6e..f3cf60d3e1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts @@ -25,9 +25,10 @@ import { Authority } from '@shared/models/authority.enum'; import { of } from 'rxjs'; @Component({ - selector: 'tb-version-info', - templateUrl: './version-info.component.html', - styleUrls: ['./home-page-widget.scss', './version-info.component.scss'] + selector: 'tb-version-info', + templateUrl: './version-info.component.html', + styleUrls: ['./home-page-widget.scss', './version-info.component.scss'], + standalone: false }) export class VersionInfoComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts index ff95a4882f..47550763d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts @@ -74,10 +74,11 @@ const horizontalBatteryDimensions = { }; @Component({ - selector: 'tb-battery-level-widget', - templateUrl: './battery-level-widget.component.html', - styleUrls: ['./battery-level-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-battery-level-widget', + templateUrl: './battery-level-widget.component.html', + styleUrls: ['./battery-level-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts index 2003d8a4cb..fad6b59172 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts @@ -71,10 +71,11 @@ import { UnitService } from '@core/services/unit.service'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; @Component({ - selector: 'tb-liquid-level-widget', - templateUrl: './liquid-level-widget.component.html', - styleUrls: ['./liquid-level-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-liquid-level-widget', + templateUrl: './liquid-level-widget.component.html', + styleUrls: ['./liquid-level-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class LiquidLevelWidgetComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.ts index 9baa7e5e10..be4d992330 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.ts @@ -76,10 +76,11 @@ const shapeHeight = 113; const shapeAspect = shapeWidth / shapeHeight; @Component({ - selector: 'tb-signal-strength-widget', - templateUrl: './signal-strength-widget.component.html', - styleUrls: ['./signal-strength-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-signal-strength-widget', + templateUrl: './signal-strength-widget.component.html', + styleUrls: ['./signal-strength-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class SignalStrengthWidgetComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts index dd0a776939..e59570d44a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts @@ -48,10 +48,11 @@ import { ValueType } from '@shared/models/constants'; const initialStatusWidgetSize = 147; @Component({ - selector: 'tb-status-widget', - templateUrl: './status-widget.component.html', - styleUrls: ['../action/action-widget.scss', './status-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-status-widget', + templateUrl: './status-widget.component.html', + styleUrls: ['../action/action-widget.scss', './status-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class StatusWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts index 3aa5f0cd71..f6a6bb0ad3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts @@ -49,9 +49,10 @@ interface JsonInputWidgetSettings { } @Component({ - selector: 'tb-json-input-widget ', - templateUrl: './json-input-widget.component.html', - styleUrls: ['./json-input-widget.component.scss'] + selector: 'tb-json-input-widget ', + templateUrl: './json-input-widget.component.html', + styleUrls: ['./json-input-widget.component.scss'], + standalone: false }) export class JsonInputWidgetComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts index 41551b98f3..b570910fb7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts @@ -18,9 +18,10 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { LegendConfig, LegendData, LegendDirection, LegendKey, LegendPosition } from '@shared/models/widget.models'; @Component({ - selector: 'tb-legend', - templateUrl: './legend.component.html', - styleUrls: ['./legend.component.scss'] + selector: 'tb-legend', + templateUrl: './legend.component.html', + styleUrls: ['./legend.component.scss'], + standalone: false }) export class LegendComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.ts index d79a6d4dbd..71cfb95ebe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.ts @@ -28,9 +28,10 @@ export interface SelectEntityDialogData { } @Component({ - selector: 'tb-select-entity-dialog', - templateUrl: './select-entity-dialog.component.html', - styleUrls: ['./select-entity-dialog.component.scss'] + selector: 'tb-select-entity-dialog', + templateUrl: './select-entity-dialog.component.html', + styleUrls: ['./select-entity-dialog.component.scss'], + standalone: false }) export class SelectEntityDialogComponent extends DialogComponent { selectEntityFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index ac55e9e5ab..e9b0e761f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -40,10 +40,11 @@ import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ - selector: 'tb-map-widget', - templateUrl: './map-widget.component.html', - styleUrls: ['./map-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-widget', + templateUrl: './map-widget.component.html', + styleUrls: ['./map-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapWidgetComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts index fceec947af..30ba8f0f40 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts @@ -34,10 +34,11 @@ import { filter } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-map-timeline-panel', - templateUrl: './map-timeline-panel.component.html', - styleUrls: ['./map-timeline-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-timeline-panel', + templateUrl: './map-timeline-panel.component.html', + styleUrls: ['./map-timeline-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapTimelinePanelComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts index 6165a2da91..ffeca2728b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts @@ -20,11 +20,12 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer'; @Component({ - selector: 'tb-select-map-entity-panel', - templateUrl: './select-map-entity-panel.component.html', - providers: [], - styleUrls: ['./select-map-entity-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-select-map-entity-panel', + templateUrl: './select-map-entity-panel.component.html', + providers: [], + styleUrls: ['./select-map-entity-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class SelectMapEntityPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index faeeb0bbf2..5f653c38d9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -50,8 +50,9 @@ interface MarkdownWidgetSettings { type MarkdownTextFunction = (data: FormattedData[], ctx: WidgetContext) => string; @Component({ - selector: 'tb-markdown-widget', - templateUrl: './markdown-widget.component.html' + selector: 'tb-markdown-widget', + templateUrl: './markdown-widget.component.html', + standalone: false }) export class MarkdownWidgetComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index f0a843688c..7324d16672 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -30,9 +30,10 @@ import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ - selector: 'tb-mobile-app-qrcode-widget', - templateUrl: './mobile-app-qrcode-widget.component.html', - styleUrls: ['./mobile-app-qrcode-widget.component.scss'] + selector: 'tb-mobile-app-qrcode-widget', + templateUrl: './mobile-app-qrcode-widget.component.html', + styleUrls: ['./mobile-app-qrcode-widget.component.scss'], + standalone: false }) export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts index b09d5f9972..5abdbc495e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts @@ -134,9 +134,10 @@ interface MultipleInputWidgetSource { } @Component({ - selector: 'tb-multiple-input-widget ', - templateUrl: './multiple-input-widget.component.html', - styleUrls: ['./multiple-input-widget.component.scss'] + selector: 'tb-multiple-input-widget ', + templateUrl: './multiple-input-widget.component.html', + styleUrls: ['./multiple-input-widget.component.scss'], + standalone: false }) export class MultipleInputWidgetComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.ts index 6dc1dfe292..089c5e11d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.ts @@ -29,9 +29,10 @@ interface NavigationCardWidgetSettings { } @Component({ - selector: 'tb-navigation-card-widget', - templateUrl: './navigation-card-widget.component.html', - styleUrls: ['./navigation-card-widget.component.scss'] + selector: 'tb-navigation-card-widget', + templateUrl: './navigation-card-widget.component.html', + styleUrls: ['./navigation-card-widget.component.scss'], + standalone: false }) export class NavigationCardWidgetComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts index f2953630e5..fc1b60f096 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts @@ -30,9 +30,10 @@ interface NavigationCardsWidgetSettings { } @Component({ - selector: 'tb-navigation-cards-widget', - templateUrl: './navigation-cards-widget.component.html', - styleUrls: ['./navigation-cards-widget.component.scss'] + selector: 'tb-navigation-cards-widget', + templateUrl: './navigation-cards-widget.component.html', + styleUrls: ['./navigation-cards-widget.component.scss'], + standalone: false }) export class NavigationCardsWidgetComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts index caeb87cfa5..060a74aebd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts @@ -51,10 +51,11 @@ interface PhotoCameraInputWidgetSettings { // @dynamic @Component({ - selector: 'tb-photo-camera-widget', - templateUrl: './photo-camera-input.component.html', - styleUrls: ['./photo-camera-input.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-photo-camera-widget', + templateUrl: './photo-camera-input.component.html', + styleUrls: ['./photo-camera-input.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class PhotoCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts index e253485b1b..5426266e35 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts @@ -44,9 +44,10 @@ interface QrCodeWidgetSettings { type QrCodeTextFunction = (data: FormattedData[]) => string; @Component({ - selector: 'tb-qrcode-widget', - templateUrl: './qrcode-widget.component.html', - styleUrls: [] + selector: 'tb-qrcode-widget', + templateUrl: './qrcode-widget.component.html', + styleUrls: [], + standalone: false }) export class QrCodeWidgetComponent extends PageComponent implements OnInit, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts index 127306db01..cdaff8fd15 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts @@ -30,9 +30,10 @@ import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget import GenericOptions = CanvasGauges.GenericOptions; @Component({ - selector: 'tb-knob', - templateUrl: './knob.component.html', - styleUrls: ['./knob.component.scss'] + selector: 'tb-knob', + templateUrl: './knob.component.html', + styleUrls: ['./knob.component.scss'], + standalone: false }) export class KnobComponent extends BasicActionWidgetComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts index 8c480e70ef..6230020aa0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts @@ -46,9 +46,10 @@ interface LedIndicatorSettings { } @Component({ - selector: 'tb-led-indicator', - templateUrl: './led-indicator.component.html', - styleUrls: ['./led-indicator.component.scss'] + selector: 'tb-led-indicator', + templateUrl: './led-indicator.component.html', + styleUrls: ['./led-indicator.component.scss'], + standalone: false }) export class LedIndicatorComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts index 2f759f7c25..ef00b39a65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts @@ -26,9 +26,10 @@ import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-persistent-add-dialog', - templateUrl: './persistent-add-dialog.component.html', - styleUrls: ['./persistent-add-dialog.component.scss'] + selector: 'tb-persistent-add-dialog', + templateUrl: './persistent-add-dialog.component.html', + styleUrls: ['./persistent-add-dialog.component.scss'], + standalone: false }) export class PersistentAddDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts index 9eec2c9a37..0cb7e874c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts @@ -34,9 +34,10 @@ export interface PersistentDetailsDialogData { } @Component({ - selector: 'tb-persistent-details-dialog', - templateUrl: './persistent-details-dialog.component.html', - styleUrls: ['./persistent-details-dialog.component.scss'] + selector: 'tb-persistent-details-dialog', + templateUrl: './persistent-details-dialog.component.html', + styleUrls: ['./persistent-details-dialog.component.scss'], + standalone: false }) export class PersistentDetailsDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts index 1215a60f8c..50eb4dddc9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts @@ -27,9 +27,10 @@ export interface PersistentFilterPanelData { } @Component({ - selector: 'tb-persistent-filter-panel', - templateUrl: './persistent-filter-panel.component.html', - styleUrls: ['./persistent-filter-panel.component.scss'] + selector: 'tb-persistent-filter-panel', + templateUrl: './persistent-filter-panel.component.html', + styleUrls: ['./persistent-filter-panel.component.scss'], + standalone: false }) export class PersistentFilterPanelComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts index 0452a07422..225cfe6a4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -98,9 +98,10 @@ interface PersistentTableWidgetActionDescriptor extends TableCellButtonActionDes } @Component({ - selector: 'tb-persistent-table-widget', - templateUrl: './persistent-table.component.html', - styleUrls: ['./persistent-table.component.scss' , '../table-widget.scss'] + selector: 'tb-persistent-table-widget', + templateUrl: './persistent-table.component.html', + styleUrls: ['./persistent-table.component.scss', '../table-widget.scss'], + standalone: false }) export class PersistentTableComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts index 4b7bb9922b..acfb9b209e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts @@ -42,10 +42,11 @@ import { SVG, Svg } from '@svgdotjs/svg.js'; import { MatIconRegistry } from '@angular/material/icon'; @Component({ - selector: 'tb-power-button-widget', - templateUrl: './power-button-widget.component.html', - styleUrls: ['../action/action-widget.scss', './power-button-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-power-button-widget', + templateUrl: './power-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './power-button-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class PowerButtonWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts index 2d9b7b3d19..d397b47e1c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts @@ -42,9 +42,10 @@ interface RoundSwitchSettings { } @Component({ - selector: 'tb-round-switch', - templateUrl: './round-switch.component.html', - styleUrls: ['./round-switch.component.scss'] + selector: 'tb-round-switch', + templateUrl: './round-switch.component.html', + styleUrls: ['./round-switch.component.scss'], + standalone: false }) export class RoundSwitchComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts index 1026d1f73e..ef9c1ff826 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts @@ -50,10 +50,11 @@ const horizontalLayoutPadding = 48; const verticalLayoutPadding = 36; @Component({ - selector: 'tb-single-switch-widget', - templateUrl: './single-switch-widget.component.html', - styleUrls: ['../action/action-widget.scss', './single-switch-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-single-switch-widget', + templateUrl: './single-switch-widget.component.html', + styleUrls: ['../action/action-widget.scss', './single-switch-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class SingleSwitchWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.ts index 14278c98e4..a1c25cbd39 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.ts @@ -50,10 +50,11 @@ import tinycolor from 'tinycolor2'; import { UnitService } from '@core/services/unit.service'; @Component({ - selector: 'tb-slider-widget', - templateUrl: './slider-widget.component.html', - styleUrls: ['../action/action-widget.scss', './slider-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-slider-widget', + templateUrl: './slider-widget.component.html', + styleUrls: ['../action/action-widget.scss', './slider-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class SliderWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts index 52c1149808..d7056558a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts @@ -51,9 +51,10 @@ interface SwitchSettings { } @Component({ - selector: 'tb-switch', - templateUrl: './switch.component.html', - styleUrls: ['./switch.component.scss'] + selector: 'tb-switch', + templateUrl: './switch.component.html', + styleUrls: ['./switch.component.scss'], + standalone: false }) export class SwitchComponent extends PageComponent implements AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts index 4c462a5e56..8ccda39a54 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts @@ -57,10 +57,11 @@ import { UtilsService } from '@core/services/utils.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-value-stepper-widget', - templateUrl: './value-stepper-widget.component.html', - styleUrls: ['../action/action-widget.scss', './value-stepper-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-value-stepper-widget', + templateUrl: './value-stepper-widget.component.html', + styleUrls: ['../action/action-widget.scss', './value-stepper-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ValueStepperWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts index 2edf94b94f..5e13120800 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts @@ -44,10 +44,11 @@ import { MatIconRegistry } from '@angular/material/icon'; import { RafService } from '@core/services/raf.service'; @Component({ - selector: 'tb-scada-symbol-widget', - templateUrl: './scada-symbol-widget.component.html', - styleUrls: ['../action/action-widget.scss', './scada-symbol-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol-widget', + templateUrl: './scada-symbol-widget.component.html', + styleUrls: ['../action/action-widget.scss', './scada-symbol-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolWidgetComponent implements OnInit, AfterViewInit, OnDestroy, ScadaSymbolObjectCallbacks { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarm-count-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarm-count-widget-settings.component.ts index 449319df59..4c7d90bf77 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarm-count-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarm-count-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { countDefaultSettings } from '@home/components/widget/lib/count/count-widget.models'; @Component({ - selector: 'tb-alarm-count-widget-settings', - templateUrl: './alarm-count-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-alarm-count-widget-settings', + templateUrl: './alarm-count-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class AlarmCountWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts index 3e183bcba6..08cc62b33f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-alarms-table-key-settings', - templateUrl: './alarms-table-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-alarms-table-key-settings', + templateUrl: './alarms-table-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class AlarmsTableKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts index 7dabd06ea1..79fc3d1f59 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models'; @Component({ - selector: 'tb-alarms-table-widget-settings', - templateUrl: './alarms-table-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-alarms-table-widget-settings', + templateUrl: './alarms-table-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.ts index cc7a73294a..6c85eb8e22 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.ts @@ -24,9 +24,10 @@ import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.m import { actionButtonDefaultSettings } from '@home/components/widget/lib/button/action-button-widget.models'; @Component({ - selector: 'tb-action-button-widget-settings', - templateUrl: './action-button-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-action-button-widget-settings', + templateUrl: './action-button-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class ActionButtonWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/command-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/command-button-widget-settings.component.ts index 304d4b91a0..9f82a93285 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/command-button-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/command-button-widget-settings.component.ts @@ -23,9 +23,10 @@ import { ValueType } from '@shared/models/constants'; import { commandButtonDefaultSettings } from '@home/components/widget/lib/button/command-button-widget.models'; @Component({ - selector: 'tb-command-button-widget-settings', - templateUrl: './command-button-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-command-button-widget-settings', + templateUrl: './command-button-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class CommandButtonWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts index df83f077d9..4c95654fdb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts @@ -28,9 +28,10 @@ import { } from '@home/components/widget/lib/rpc/power-button-widget.models'; @Component({ - selector: 'tb-power-button-widget-settings', - templateUrl: './power-button-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-power-button-widget-settings', + templateUrl: './power-button-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class PowerButtonWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts index 540688c18e..629ddd179e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts @@ -41,9 +41,10 @@ import { import { ValueCardLayout } from '@home/components/widget/lib/cards/value-card-widget.models'; @Component({ - selector: 'tb-segmented-button-widget-settings', - templateUrl: './segmented-button-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-segmented-button-widget-settings', + templateUrl: './segmented-button-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class SegmentedButtonWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/toggle-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/toggle-button-widget-settings.component.ts index c14616ee7f..e8bb057b70 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/toggle-button-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/toggle-button-widget-settings.component.ts @@ -25,9 +25,10 @@ import { toggleButtonDefaultSettings } from '@home/components/widget/lib/button/ type ButtonAppearanceType = 'checked' | 'unchecked'; @Component({ - selector: 'tb-toggle-button-widget-settings', - templateUrl: './toggle-button-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-toggle-button-widget-settings', + templateUrl: './toggle-button-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class ToggleButtonWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts index c1088cecab..0744461c2d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts @@ -26,9 +26,10 @@ import { } from '@home/components/widget/lib/cards/aggregated-value-card.models'; @Component({ - selector: 'tb-aggregated-value-card-key-settings', - templateUrl: './aggregated-value-card-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-aggregated-value-card-key-settings', + templateUrl: './aggregated-value-card-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class AggregatedValueCardKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts index 5bc3ac954d..3afdc7e6cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts @@ -23,9 +23,10 @@ import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-s import { aggregatedValueCardDefaultSettings } from '@home/components/widget/lib/cards/aggregated-value-card.models'; @Component({ - selector: 'tb-aggregated-value-card-widget-settings', - templateUrl: './aggregated-value-card-widget-settings.component.html', - styleUrls: [] + selector: 'tb-aggregated-value-card-widget-settings', + templateUrl: './aggregated-value-card-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class AggregatedValueCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts index aed354bb69..3ffff5309f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts @@ -42,17 +42,18 @@ import { import { Observable, of } from "rxjs"; @Component({ - selector: 'tb-api-usage-data-key-row', - templateUrl: './api-usage-data-key-row.component.html', - styleUrls: ['./api-usage-data-key-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ApiUsageDataKeyRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-api-usage-data-key-row', + templateUrl: './api-usage-data-key-row.component.html', + styleUrls: ['./api-usage-data-key-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ApiUsageDataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ApiUsageDataKeyRowComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts index b89679de5b..5bda37fac1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts @@ -48,16 +48,17 @@ import { MatDialog } from '@angular/material/dialog'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ - selector: 'tb-api-usage-widget-settings', - templateUrl: './api-usage-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss', 'api-usage-widget-settings.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ApiUsageWidgetSettingsComponent), - multi: true - } - ], + selector: 'tb-api-usage-widget-settings', + templateUrl: './api-usage-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss', 'api-usage-widget-settings.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ApiUsageWidgetSettingsComponent), + multi: true + } + ], + standalone: false }) export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.ts index 5fa4a4b5fa..181ef7ddaf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.ts @@ -23,9 +23,10 @@ import { Observable, of } from 'rxjs'; import { map, mergeMap, startWith } from 'rxjs/operators'; @Component({ - selector: 'tb-dashboard-state-widget-settings', - templateUrl: './dashboard-state-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-dashboard-state-widget-settings', + templateUrl: './dashboard-state-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class DashboardStateWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.ts index 448476f707..6c6691d65c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-edge-quick-overview-widget-settings', - templateUrl: './edge-quick-overview-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-edge-quick-overview-widget-settings', + templateUrl: './edge-quick-overview-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class EdgeQuickOverviewWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/html-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/html-card-widget-settings.component.ts index 0fa63f4cd1..4cdb47b1dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/html-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/html-card-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-html-card-widget-settings', - templateUrl: './html-card-widget-settings.component.html', - styleUrls: [] + selector: 'tb-html-card-widget-settings', + templateUrl: './html-card-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class HtmlCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts index 51342a4944..1472a58ab1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-card-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { labelCardWidgetDefaultSettings } from '@home/components/widget/lib/cards/label-card-widget.models'; @Component({ - selector: 'tb-label-card-widget-settings', - templateUrl: './label-card-widget-settings.component.html', - styleUrls: [] + selector: 'tb-label-card-widget-settings', + templateUrl: './label-card-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class LabelCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts index d90bf9571b..5bf0ace732 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-value-card-widget-settings.component.ts @@ -24,9 +24,10 @@ import { formatValue } from '@core/utils'; import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-label-value-card-widget-settings', - templateUrl: './label-value-card-widget-settings.component.html', - styleUrls: [] + selector: 'tb-label-value-card-widget-settings', + templateUrl: './label-value-card-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class LabelValueCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts index 26b995c7a2..f0a6bd1ce4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts @@ -32,16 +32,17 @@ export interface LabelWidgetLabel { } @Component({ - selector: 'tb-label-widget-label', - templateUrl: './label-widget-label.component.html', - styleUrls: ['./label-widget-label.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => LabelWidgetLabelComponent), - multi: true - } - ] + selector: 'tb-label-widget-label', + templateUrl: './label-widget-label.component.html', + styleUrls: ['./label-widget-label.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LabelWidgetLabelComponent), + multi: true + } + ], + standalone: false }) export class LabelWidgetLabelComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts index 01a8862307..88e3e23cf2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts @@ -23,9 +23,10 @@ import { LabelWidgetLabel } from '@home/components/widget/lib/settings/cards/lab import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ - selector: 'tb-label-widget-settings', - templateUrl: './label-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-label-widget-settings', + templateUrl: './label-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class LabelWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts index 997db94c2c..0fed9263e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-markdown-widget-settings', - templateUrl: './markdown-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-markdown-widget-settings', + templateUrl: './markdown-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class MarkdownWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts index a4ad7933f3..82aea1e4a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts @@ -23,9 +23,10 @@ import { badgePositionTranslationsMap } from '@shared/models/mobile-app.models'; import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; @Component({ - selector: 'tb-mobile-app-qr-code-widget-settings', - templateUrl: './mobile-app-qr-code-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-mobile-app-qr-code-widget-settings', + templateUrl: './mobile-app-qr-code-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.ts index e425d17893..80d8c79141 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.ts @@ -30,9 +30,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-progress-bar-widget-settings', - templateUrl: './progress-bar-widget-settings.component.html', - styleUrls: [] + selector: 'tb-progress-bar-widget-settings', + templateUrl: './progress-bar-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class ProgressBarWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.ts index 62fdc7a487..b93a95b75e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-qrcode-widget-settings', - templateUrl: './qrcode-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-qrcode-widget-settings', + templateUrl: './qrcode-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts index e6e796e863..54722150f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-simple-card-widget-settings', - templateUrl: './simple-card-widget-settings.component.html', - styleUrls: [] + selector: 'tb-simple-card-widget-settings', + templateUrl: './simple-card-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts index e8ab4f73fa..e49cb5b4e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-timeseries-table-key-settings', - templateUrl: './timeseries-table-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-timeseries-table-key-settings', + templateUrl: './timeseries-table-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts index 842cf779e6..918ff9a7b8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-timeseries-table-latest-key-settings', - templateUrl: './timeseries-table-latest-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-timeseries-table-latest-key-settings', + templateUrl: './timeseries-table-latest-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts index a2b86cab55..32d2b1b087 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts @@ -24,9 +24,10 @@ import { Direction } from '@shared/models/page/sort-order'; import { entityFields } from '@shared/models/entity.models'; @Component({ - selector: 'tb-timeseries-table-widget-settings', - templateUrl: './timeseries-table-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-timeseries-table-widget-settings', + templateUrl: './timeseries-table-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.ts index 6a4482012e..c0baba8794 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { unreadNotificationDefaultSettings } from '@home/components/widget/lib/cards/unread-notification-widget.models'; @Component({ - selector: 'tb-unread-notification-widget-settings', - templateUrl: './unread-notification-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-unread-notification-widget-settings', + templateUrl: './unread-notification-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UnreadNotificationWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts index 18ba0bdca9..00614b7f40 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts @@ -32,9 +32,10 @@ import { DateFormatProcessor, DateFormatSettings, getLabel } from '@shared/model import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-value-card-widget-settings', - templateUrl: './value-card-widget-settings.component.html', - styleUrls: [] + selector: 'tb-value-card-widget-settings', + templateUrl: './value-card-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class ValueCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.ts index 46878d0fcd..d218757030 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.ts @@ -29,9 +29,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-value-chart-card-widget-settings', - templateUrl: './value-chart-card-widget-settings.component.html', - styleUrls: [] + selector: 'tb-value-chart-card-widget-settings', + templateUrl: './value-chart-card-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class ValueChartCardWidgetSettingsComponent extends WidgetSettingsComponent { 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 ace61904cc..432007de53 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 @@ -28,9 +28,10 @@ import { } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; @Component({ - selector: 'tb-bar-chart-widget-settings', - templateUrl: './latest-chart-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-bar-chart-widget-settings', + templateUrl: './latest-chart-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class BarChartWidgetSettingsComponent extends LatestChartWidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts index cc0c5c03cd..6290572770 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts @@ -33,9 +33,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-bar-chart-with-labels-widget-settings', - templateUrl: './bar-chart-with-labels-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-bar-chart-with-labels-widget-settings', + templateUrl: './bar-chart-with-labels-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/chart-widget-settings.component.ts index 7152a6a39e..50d407948b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/chart-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-chart-widget-settings', - templateUrl: './chart-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-chart-widget-settings', + templateUrl: './chart-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class ChartWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component.ts index 12e39a52d7..88226f4c28 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-doughnut-chart-widget-settings', - templateUrl: './doughnut-chart-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-doughnut-chart-widget-settings', + templateUrl: './doughnut-chart-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class DoughnutChartWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts index 6ba0508c95..17a0d60aab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts @@ -29,9 +29,10 @@ import { } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; @Component({ - selector: 'tb-doughnut-widget-settings', - templateUrl: './latest-chart-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-doughnut-widget-settings', + templateUrl: './latest-chart-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class DoughnutWidgetSettingsComponent extends LatestChartWidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.ts index bc2e7c22db..b2c0a34596 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { flotDataKeyDefaultSettings } from '@home/components/widget/lib/settings/chart/flot-key-settings.component'; @Component({ - selector: 'tb-flot-bar-key-settings', - templateUrl: './flot-bar-key-settings.component.html', - styleUrls: [] + selector: 'tb-flot-bar-key-settings', + templateUrl: './flot-bar-key-settings.component.html', + styleUrls: [], + standalone: false }) export class FlotBarKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.ts index c6720ecd13..c74fad3cda 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { flotDefaultSettings } from '@home/components/widget/lib/settings/chart/flot-widget-settings.component'; @Component({ - selector: 'tb-flot-bar-widget-settings', - templateUrl: './flot-bar-widget-settings.component.html', - styleUrls: [] + selector: 'tb-flot-bar-widget-settings', + templateUrl: './flot-bar-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class FlotBarWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts index e5711e4af3..cd46662ae5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts @@ -91,21 +91,22 @@ export function flotDataKeyDefaultSettings(chartType: ChartType): TbFlotKeySetti } @Component({ - selector: 'tb-flot-key-settings', - templateUrl: './flot-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FlotKeySettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => FlotKeySettingsComponent), - multi: true, - } - ] + selector: 'tb-flot-key-settings', + templateUrl: './flot-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FlotKeySettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FlotKeySettingsComponent), + multi: true, + } + ], + standalone: false }) export class FlotKeySettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.ts index cc7842386e..78897b5db0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-flot-latest-key-settings', - templateUrl: './flot-latest-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-flot-latest-key-settings', + templateUrl: './flot-latest-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class FlotLatestKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.ts index 74b6ae9839..2b972a0d89 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { flotDataKeyDefaultSettings } from '@home/components/widget/lib/settings/chart/flot-key-settings.component'; @Component({ - selector: 'tb-flot-line-key-settings', - templateUrl: './flot-line-key-settings.component.html', - styleUrls: [] + selector: 'tb-flot-line-key-settings', + templateUrl: './flot-line-key-settings.component.html', + styleUrls: [], + standalone: false }) export class FlotLineKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.ts index ca01b3cbc5..3c4882a3c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-widget-settings.component.ts @@ -23,9 +23,10 @@ import { flotDefaultSettings } from '@home/components/widget/lib/settings/chart/ import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-flot-line-widget-settings', - templateUrl: './flot-line-widget-settings.component.html', - styleUrls: [] + selector: 'tb-flot-line-widget-settings', + templateUrl: './flot-line-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class FlotLineWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-key-settings.component.ts index 9d8674dddf..39f6b3333a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-key-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-flot-pie-key-settings', - templateUrl: './flot-pie-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-flot-pie-key-settings', + templateUrl: './flot-pie-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class FlotPieKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.ts index 78147a4d52..98ac711499 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-flot-pie-widget-settings', - templateUrl: './flot-pie-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-flot-pie-widget-settings', + templateUrl: './flot-pie-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class FlotPieWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts index 0ebe86668b..b8accc637b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts @@ -40,17 +40,18 @@ import { TbFlotKeyThreshold } from '@home/components/widget/lib/flot-widget.mode import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-flot-threshold', - templateUrl: './flot-threshold.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FlotThresholdComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-flot-threshold', + templateUrl: './flot-threshold.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FlotThresholdComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class FlotThresholdComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts index 73b5248f04..65fc17df26 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts @@ -106,21 +106,22 @@ export const flotDefaultSettings = (chartType: ChartType): Partial FlotWidgetSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => FlotWidgetSettingsComponent), - multi: true, - } - ] + selector: 'tb-flot-widget-settings', + templateUrl: './flot-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FlotWidgetSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FlotWidgetSettingsComponent), + multi: true, + } + ], + standalone: false }) export class FlotWidgetSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts index 6d2b50824b..71e1163d16 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts @@ -46,16 +46,17 @@ export function labelDataKeyValidator(control: AbstractControl): ValidationError } @Component({ - selector: 'tb-label-data-key', - templateUrl: './label-data-key.component.html', - styleUrls: ['./label-data-key.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => LabelDataKeyComponent), - multi: true - } - ] + selector: 'tb-label-data-key', + templateUrl: './label-data-key.component.html', + styleUrls: ['./label-data-key.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LabelDataKeyComponent), + multi: true + } + ], + standalone: false }) export class LabelDataKeyComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.ts index 85db032513..9a7c2318bf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.ts @@ -28,9 +28,10 @@ import { } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; @Component({ - selector: 'tb-pie-chart-widget-settings', - templateUrl: './latest-chart-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-pie-chart-widget-settings', + templateUrl: './latest-chart-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class PieChartWidgetSettingsComponent extends LatestChartWidgetSettingsComponent { 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 f452fee0bf..e941e27f13 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 @@ -28,9 +28,10 @@ import { } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; @Component({ - selector: 'tb-polar-area-chart-widget-settings', - templateUrl: './latest-chart-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-polar-area-chart-widget-settings', + templateUrl: './latest-chart-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class PolarAreaChartWidgetSettingsComponent extends LatestChartWidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/radar-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/radar-chart-widget-settings.component.ts index 6e15a4c2bc..f9d3128027 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/radar-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/radar-chart-widget-settings.component.ts @@ -28,9 +28,10 @@ import { } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; @Component({ - selector: 'tb-radar-chart-widget-settings', - templateUrl: './latest-chart-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-radar-chart-widget-settings', + templateUrl: './latest-chart-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class RadarChartWidgetSettingsComponent extends LatestChartWidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts index 5b1b20c2fd..703c621d62 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts @@ -43,9 +43,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-range-chart-widget-settings', - templateUrl: './range-chart-widget-settings.component.html', - styleUrls: [] + selector: 'tb-range-chart-widget-settings', + templateUrl: './range-chart-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts index 3c99b96c07..34b0edb6ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts @@ -35,9 +35,10 @@ import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart import { WidgetService } from '@core/http/widget.service'; @Component({ - selector: 'tb-time-series-chart-key-settings', - templateUrl: './time-series-chart-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-time-series-chart-key-settings', + templateUrl: './time-series-chart-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts index 05afef0af8..c2e024bb55 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts @@ -45,16 +45,17 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { getSourceTbUnitSymbol, isNotEmptyTbUnits } from '@shared/models/unit.models'; @Component({ - selector: 'tb-time-series-chart-line-settings', - templateUrl: './time-series-chart-line-settings.component.html', - styleUrls: ['./../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartLineSettingsComponent), - multi: true - } - ] + selector: 'tb-time-series-chart-line-settings', + templateUrl: './time-series-chart-line-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartLineSettingsComponent), + multi: true + } + ], + standalone: false }) export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index 4abc0bf437..8a96f31445 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -42,9 +42,10 @@ import { WidgetService } from '@core/http/widget.service'; import { TimeSeriesChartTooltipTrigger } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models'; @Component({ - selector: 'tb-time-series-chart-widget-settings', - templateUrl: './time-series-chart-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-time-series-chart-widget-settings', + templateUrl: './time-series-chart-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts index 97d7bfed25..2537cbe310 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts @@ -32,17 +32,18 @@ import { } from '@home/components/widget/lib/settings/common/action/custom-action.models'; @Component({ - selector: 'tb-custom-action-pretty-editor', - templateUrl: './custom-action-pretty-editor.component.html', - styleUrls: ['./custom-action-pretty-editor.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CustomActionPrettyEditorComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-custom-action-pretty-editor', + templateUrl: './custom-action-pretty-editor.component.html', + styleUrls: ['./custom-action-pretty-editor.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CustomActionPrettyEditorComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class CustomActionPrettyEditorComponent implements AfterViewInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts index 7cc295e94e..dfd03e406b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts @@ -41,10 +41,11 @@ import { getAce } from '@shared/models/ace/ace.models'; import { beautifyCss, beautifyHtml } from '@shared/models/beautify.models'; @Component({ - selector: 'tb-custom-action-pretty-resources-tabs', - templateUrl: './custom-action-pretty-resources-tabs.component.html', - styleUrls: ['./custom-action-pretty-resources-tabs.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-custom-action-pretty-resources-tabs', + templateUrl: './custom-action-pretty-resources-tabs.component.html', + styleUrls: ['./custom-action-pretty-resources-tabs.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class CustomActionPrettyResourcesTabsComponent extends PageComponent implements OnInit, OnChanges, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts index 96915e5712..cb384248c5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts @@ -38,11 +38,12 @@ import { EntityType } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-get-value-action-settings-panel', - templateUrl: './get-value-action-settings-panel.component.html', - providers: [], - styleUrls: ['./action-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-get-value-action-settings-panel', + templateUrl: './get-value-action-settings-panel.component.html', + providers: [], + styleUrls: ['./action-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class GetValueActionSettingsPanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts index d27efc1081..8b1e18a917 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts @@ -38,17 +38,18 @@ import { IAliasController } from '@core/api/widget-api.models'; import { TargetDevice, widgetType } from '@shared/models/widget.models'; @Component({ - selector: 'tb-get-value-action-settings', - templateUrl: './action-settings-button.component.html', - styleUrls: ['./action-settings-button.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => GetValueActionSettingsComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-get-value-action-settings', + templateUrl: './action-settings-button.component.html', + styleUrls: ['./action-settings-button.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GetValueActionSettingsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class GetValueActionSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts index c6f60c1892..9a4ba9b2aa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts @@ -21,15 +21,16 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { deepTrim, isEqual } from '@core/utils'; @Component({ - selector: 'tb-map-item-tooltips', - templateUrl: './map-item-tooltips.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapItemTooltipsComponent), - multi: true - } - ] + selector: 'tb-map-item-tooltips', + templateUrl: './map-item-tooltips.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapItemTooltipsComponent), + multi: true + } + ], + standalone: false }) export class MapItemTooltipsComponent implements ControlValueAccessor, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts index 553e3c38a1..b836fe2ade 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts @@ -50,14 +50,15 @@ import { TbFunction } from '@shared/models/js-function.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-mobile-action-editor', - templateUrl: './mobile-action-editor.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MobileActionEditorComponent), - multi: true - }] + selector: 'tb-mobile-action-editor', + templateUrl: './mobile-action-editor.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MobileActionEditorComponent), + multi: true + }], + standalone: false }) export class MobileActionEditorComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts index 17a3ae5b6c..a778b9a509 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts @@ -36,11 +36,12 @@ import { ValueType } from '@shared/models/constants'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-set-value-action-settings-panel', - templateUrl: './set-value-action-settings-panel.component.html', - providers: [], - styleUrls: ['./action-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-set-value-action-settings-panel', + templateUrl: './set-value-action-settings-panel.component.html', + providers: [], + styleUrls: ['./action-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class SetValueActionSettingsPanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts index 30202ed551..6e9950033b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts @@ -39,17 +39,18 @@ import { import { ValueType } from '@shared/models/constants'; @Component({ - selector: 'tb-set-value-action-settings', - templateUrl: './action-settings-button.component.html', - styleUrls: ['./action-settings-button.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SetValueActionSettingsComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-set-value-action-settings', + templateUrl: './action-settings-button.component.html', + styleUrls: ['./action-settings-button.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SetValueActionSettingsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class SetValueActionSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts index c43b36e014..f0ba6f05ce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts @@ -23,11 +23,12 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { TranslateService } from '@ngx-translate/core'; @Component({ - selector: 'tb-widget-action-settings-panel', - templateUrl: './widget-action-settings-panel.component.html', - providers: [], - styleUrls: ['./action-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-action-settings-panel', + templateUrl: './widget-action-settings-panel.component.html', + providers: [], + styleUrls: ['./action-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetActionSettingsPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts index f60a7f1d3c..737fafd75a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts @@ -41,17 +41,18 @@ import { } from '@home/components/widget/lib/settings/common/action/widget-action-settings-panel.component'; @Component({ - selector: 'tb-widget-action-settings', - templateUrl: './action-settings-button.component.html', - styleUrls: ['./action-settings-button.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetActionSettingsComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-action-settings', + templateUrl: './action-settings-button.component.html', + styleUrls: ['./action-settings-button.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetActionSettingsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetActionSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts index 09bddd26dd..9efb80081e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts @@ -66,21 +66,22 @@ const stateDisplayTypesTranslations = new Map( ); @Component({ - selector: 'tb-widget-action', - templateUrl: './widget-action.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetActionComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => WidgetActionComponent), - multi: true, - } - ] + selector: 'tb-widget-action', + templateUrl: './widget-action.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetActionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => WidgetActionComponent), + multi: true, + } + ], + standalone: false }) export class WidgetActionComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.ts index c1e5114eb6..d2f1544014 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.ts @@ -32,16 +32,17 @@ import { Datasource } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-advanced-range', - templateUrl: './advanced-range.component.html', - styleUrls: ['./advanced-range.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AdvancedRangeComponent), - multi: true - } - ] + selector: 'tb-advanced-range', + templateUrl: './advanced-range.component.html', + styleUrls: ['./advanced-range.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AdvancedRangeComponent), + multi: true + } + ], + standalone: false }) export class AdvancedRangeComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts index 0836ad8e15..561541981f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts @@ -39,14 +39,15 @@ import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form- import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-entity-alias-select', - templateUrl: './entity-alias-select.component.html', - styleUrls: ['./entity-alias-select.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityAliasSelectComponent), - multi: true - }] + selector: 'tb-entity-alias-select', + templateUrl: './entity-alias-select.component.html', + styleUrls: ['./entity-alias-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAliasSelectComponent), + multi: true + }], + standalone: false }) export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.ts index 01f16eccc1..ab0ab9fa4b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.ts @@ -30,11 +30,12 @@ import { DatePipe } from '@angular/common'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-auto-date-format-settings-panel', - templateUrl: './auto-date-format-settings-panel.component.html', - providers: [], - styleUrls: ['./auto-date-format-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-auto-date-format-settings-panel', + templateUrl: './auto-date-format-settings-panel.component.html', + providers: [], + styleUrls: ['./auto-date-format-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class AutoDateFormatSettingsPanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts index 8d3dadb56d..59a145c66f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts @@ -25,16 +25,17 @@ import { } from '@home/components/widget/lib/settings/common/auto-date-format-settings-panel.component'; @Component({ - selector: 'tb-auto-date-format-settings', - templateUrl: './auto-date-format-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AutoDateFormatSettingsComponent), - multi: true - } - ] + selector: 'tb-auto-date-format-settings', + templateUrl: './auto-date-format-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AutoDateFormatSettingsComponent), + multi: true + } + ], + standalone: false }) export class AutoDateFormatSettingsComponent implements OnInit, ControlValueAccessor { 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 88b01c19c5..24037f2170 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 @@ -36,20 +36,21 @@ import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/k import { merge } from 'rxjs'; @Component({ - selector: 'tb-axis-scale-row', - templateUrl: './axis-scale-row.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AxisScaleRowComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AxisScaleRowComponent), - multi: true - }, - ] + selector: 'tb-axis-scale-row', + templateUrl: './axis-scale-row.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AxisScaleRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AxisScaleRowComponent), + multi: true + }, + ], + standalone: false }) export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts index b1ee8ff332..323241e0f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts @@ -42,11 +42,12 @@ import { DomSanitizer } from '@angular/platform-browser'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-background-settings-panel', - templateUrl: './background-settings-panel.component.html', - providers: [], - styleUrls: ['./background-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-background-settings-panel', + templateUrl: './background-settings-panel.component.html', + providers: [], + styleUrls: ['./background-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class BackgroundSettingsPanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts index 94737fa3b1..fd15e6da1a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts @@ -43,17 +43,18 @@ import { DomSanitizer } from '@angular/platform-browser'; import { tap } from 'rxjs/operators'; @Component({ - selector: 'tb-background-settings', - templateUrl: './background-settings.component.html', - styleUrls: ['./background-settings.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => BackgroundSettingsComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-background-settings', + templateUrl: './background-settings.component.html', + styleUrls: ['./background-settings.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BackgroundSettingsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class BackgroundSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.ts index c3bd4e3579..ccd1468b17 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.ts @@ -29,17 +29,18 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { widgetTitleAutocompleteValues } from '@shared/models/widget.models'; @Component({ - selector: 'tb-widget-button-appearance', - templateUrl: './widget-button-appearance.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetButtonAppearanceComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-button-appearance', + templateUrl: './widget-button-appearance.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetButtonAppearanceComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetButtonAppearanceComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.ts index f4d11a9d2f..1fc3c4f20c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.ts @@ -46,11 +46,12 @@ import { WidgetButtonComponent } from '@shared/components/button/widget-button.c import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-widget-button-custom-style-panel', - templateUrl: './widget-button-custom-style-panel.component.html', - providers: [], - styleUrls: ['./widget-button-custom-style-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-button-custom-style-panel', + templateUrl: './widget-button-custom-style-panel.component.html', + providers: [], + styleUrls: ['./widget-button-custom-style-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetButtonCustomStylePanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts index 9f54623698..72d3d5e5fd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts @@ -40,17 +40,18 @@ import { import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-widget-button-custom-style', - templateUrl: './widget-button-custom-style.component.html', - styleUrls: ['./widget-button-custom-style.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetButtonCustomStyleComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-button-custom-style', + templateUrl: './widget-button-custom-style.component.html', + styleUrls: ['./widget-button-custom-style.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetButtonCustomStyleComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetButtonCustomStyleComponent implements OnInit, OnChanges, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts index 6ba1021b5c..6502548bf2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts @@ -48,11 +48,12 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-widget-button-toggle-custom-style-panel', - templateUrl: './widget-button-toggle-custom-style-panel.component.html', - providers: [], - styleUrls: ['./widget-button-toggle-custom-style-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-button-toggle-custom-style-panel', + templateUrl: './widget-button-toggle-custom-style-panel.component.html', + providers: [], + styleUrls: ['./widget-button-toggle-custom-style-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetButtonToggleCustomStylePanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts index 4d7767f04a..af93d58722 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts @@ -40,17 +40,18 @@ import { } from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component'; @Component({ - selector: 'tb-widget-button-toggle-custom-style', - templateUrl: './widget-button-toggle-custom-style.component.html', - styleUrls: ['./widget-button-toggle-custom-style.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetButtonToggleCustomStyleComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-button-toggle-custom-style', + templateUrl: './widget-button-toggle-custom-style.component.html', + styleUrls: ['./widget-button-toggle-custom-style.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetButtonToggleCustomStyleComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetButtonToggleCustomStyleComponent implements OnInit, OnChanges, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-animation-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-animation-settings.component.ts index d5cab573ed..61374113bf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-animation-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-animation-settings.component.ts @@ -26,16 +26,17 @@ import { chartAnimationEasings, ChartAnimationSettings } from '@home/components/ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-chart-animation-settings', - templateUrl: './chart-animation-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ChartAnimationSettingsComponent), - multi: true - } - ] + selector: 'tb-chart-animation-settings', + templateUrl: './chart-animation-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ChartAnimationSettingsComponent), + multi: true + } + ], + standalone: false }) export class ChartAnimationSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.ts index ca60fe0db2..4a652e84b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.ts @@ -41,16 +41,17 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { getSourceTbUnitSymbol, isNotEmptyTbUnits } from '@shared/models/unit.models'; @Component({ - selector: 'tb-chart-bar-settings', - templateUrl: './chart-bar-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ChartBarSettingsComponent), - multi: true - } - ] + selector: 'tb-chart-bar-settings', + templateUrl: './chart-bar-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ChartBarSettingsComponent), + multi: true + } + ], + standalone: false }) export class ChartBarSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts index e85c8bb226..f980264f4a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts @@ -33,16 +33,17 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-chart-fill-settings', - templateUrl: './chart-fill-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ChartFillSettingsComponent), - multi: true - } - ] + selector: 'tb-chart-fill-settings', + templateUrl: './chart-fill-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ChartFillSettingsComponent), + multi: true + } + ], + standalone: false }) export class ChartFillSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts index de1ef16a43..5aad0ab4f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts @@ -25,16 +25,17 @@ import { } from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component'; @Component({ - selector: 'tb-time-series-chart-axis-settings-button', - templateUrl: './time-series-chart-axis-settings-button.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartAxisSettingsButtonComponent), - multi: true - } - ] + selector: 'tb-time-series-chart-axis-settings-button', + templateUrl: './time-series-chart-axis-settings-button.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartAxisSettingsButtonComponent), + multi: true + } + ], + standalone: false }) export class TimeSeriesChartAxisSettingsButtonComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts index bb800696f1..7c55510579 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts @@ -27,11 +27,12 @@ import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/k import { Datasource } from '@shared/models/widget.models'; @Component({ - selector: 'tb-time-series-chart-axis-settings-panel', - templateUrl: './time-series-chart-axis-settings-panel.component.html', - providers: [], - styleUrls: ['./time-series-chart-axis-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-time-series-chart-axis-settings-panel', + templateUrl: './time-series-chart-axis-settings-panel.component.html', + providers: [], + styleUrls: ['./time-series-chart-axis-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimeSeriesChartAxisSettingsPanelComponent implements OnInit { 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 f31775f22f..db0f8534a9 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 @@ -39,21 +39,22 @@ 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', - templateUrl: './time-series-chart-axis-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), - multi: true - } - ] + selector: 'tb-time-series-chart-axis-settings', + templateUrl: './time-series-chart-axis-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), + multi: true + } + ], + standalone: false }) export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts index 1ad240f8b0..7dd1354cd0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts @@ -27,16 +27,17 @@ import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-time-series-chart-grid-settings', - templateUrl: './time-series-chart-grid-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartGridSettingsComponent), - multi: true - } - ] + selector: 'tb-time-series-chart-grid-settings', + templateUrl: './time-series-chart-grid-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartGridSettingsComponent), + multi: true + } + ], + standalone: false }) export class TimeSeriesChartGridSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts index 1e48cb6def..b3997331f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts @@ -41,17 +41,18 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-time-series-chart-state-row', - templateUrl: './time-series-chart-state-row.component.html', - styleUrls: ['./time-series-chart-state-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartStateRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-time-series-chart-state-row', + templateUrl: './time-series-chart-state-row.component.html', + styleUrls: ['./time-series-chart-state-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartStateRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimeSeriesChartStateRowComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts index 1de5e8019f..aaa9d6d9f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts @@ -35,22 +35,23 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-time-series-chart-states-panel', - templateUrl: './time-series-chart-states-panel.component.html', - styleUrls: ['./time-series-chart-states-panel.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-time-series-chart-states-panel', + templateUrl: './time-series-chart-states-panel.component.html', + styleUrls: ['./time-series-chart-states-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimeSeriesChartStatesPanelComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts index 37ae10c330..136ad2c728 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts @@ -56,17 +56,18 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-time-series-chart-threshold-row', - templateUrl: './time-series-chart-threshold-row.component.html', - styleUrls: ['./time-series-chart-threshold-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartThresholdRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-time-series-chart-threshold-row', + templateUrl: './time-series-chart-threshold-row.component.html', + styleUrls: ['./time-series-chart-threshold-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartThresholdRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimeSeriesChartThresholdRowComponent implements ControlValueAccessor, OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.ts index 916ba3dd53..57001605a6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.ts @@ -38,11 +38,12 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { getSourceTbUnitSymbol, isNotEmptyTbUnits, TbUnit } from '@shared/models/unit.models'; @Component({ - selector: 'tb-time-series-chart-threshold-settings-panel', - templateUrl: './time-series-chart-threshold-settings-panel.component.html', - providers: [], - styleUrls: ['./time-series-chart-threshold-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-time-series-chart-threshold-settings-panel', + templateUrl: './time-series-chart-threshold-settings-panel.component.html', + providers: [], + styleUrls: ['./time-series-chart-threshold-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimeSeriesChartThresholdSettingsPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts index 1757b14b77..3195c3a944 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts @@ -30,16 +30,17 @@ import { } from '@home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component'; @Component({ - selector: 'tb-time-series-chart-threshold-settings', - templateUrl: './time-series-chart-threshold-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartThresholdSettingsComponent), - multi: true - } - ] + selector: 'tb-time-series-chart-threshold-settings', + templateUrl: './time-series-chart-threshold-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartThresholdSettingsComponent), + multi: true + } + ], + standalone: false }) export class TimeSeriesChartThresholdSettingsComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts index f7b9e157e1..944c28b4d8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts @@ -43,22 +43,23 @@ import { ValueSourceType } from '@shared/models/widget-settings.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-time-series-chart-thresholds-panel', - templateUrl: './time-series-chart-thresholds-panel.component.html', - styleUrls: ['./time-series-chart-thresholds-panel.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartThresholdsPanelComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TimeSeriesChartThresholdsPanelComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-time-series-chart-thresholds-panel', + templateUrl: './time-series-chart-thresholds-panel.component.html', + styleUrls: ['./time-series-chart-thresholds-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartThresholdsPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartThresholdsPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimeSeriesChartThresholdsPanelComponent implements ControlValueAccessor, OnInit, Validator { 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 1d4b93c712..694e856bfa 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 @@ -53,22 +53,23 @@ import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/k import { DataKey, DataKeyType, Datasource, ValueSourceConfig, ValueSourceType } from '@app/shared/public-api'; @Component({ - selector: 'tb-time-series-chart-y-axes-panel', - templateUrl: './time-series-chart-y-axes-panel.component.html', - styleUrls: ['./time-series-chart-y-axes-panel.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartYAxesPanelComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TimeSeriesChartYAxesPanelComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-time-series-chart-y-axes-panel', + templateUrl: './time-series-chart-y-axes-panel.component.html', + styleUrls: ['./time-series-chart-y-axes-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartYAxesPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartYAxesPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, OnInit, Validator { 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 54d54d1a98..87a0414ca5 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 @@ -46,17 +46,18 @@ import { TimeSeriesChartYAxesPanelComponent } from '@home/components/widget/lib/ import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ - selector: 'tb-time-series-chart-y-axis-row', - templateUrl: './time-series-chart-y-axis-row.component.html', - styleUrls: ['./time-series-chart-y-axis-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesChartYAxisRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-time-series-chart-y-axis-row', + templateUrl: './time-series-chart-y-axis-row.component.html', + styleUrls: ['./time-series-chart-y-axis-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartYAxisRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.ts index 320b239c22..1e078d995f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.ts @@ -33,16 +33,17 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-time-series-no-aggregation-bar-width-settings', - templateUrl: './time-series-no-aggregation-bar-width-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TimeSeriesNoAggregationBarWidthSettingsComponent), - multi: true - } - ] + selector: 'tb-time-series-no-aggregation-bar-width-settings', + templateUrl: './time-series-no-aggregation-bar-width-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesNoAggregationBarWidthSettingsComponent), + multi: true + } + ], + standalone: false }) export class TimeSeriesNoAggregationBarWidthSettingsComponent implements OnInit, ControlValueAccessor { 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 2e76fffa3a..446314dc4c 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 @@ -53,17 +53,18 @@ export function advancedRangeValidator(control: AbstractControl): ValidationErro } @Component({ - selector: 'tb-color-range-list', - templateUrl: './color-range-list.component.html', - styleUrls: ['color-settings-panel.component.scss', 'color-range-list.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ColorRangeListComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-color-range-list', + templateUrl: './color-range-list.component.html', + styleUrls: ['color-settings-panel.component.scss', 'color-range-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ColorRangeListComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ColorRangeListComponent implements OnInit, ControlValueAccessor, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts index 68485e8572..b6f1c02bfb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.ts @@ -27,11 +27,12 @@ import { } from '@home/components/widget/lib/settings/common/color-range-settings.component'; @Component({ - selector: 'tb-color-range-panel', - templateUrl: './color-range-panel.component.html', - providers: [], - styleUrls: ['./color-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-color-range-panel', + templateUrl: './color-range-panel.component.html', + providers: [], + styleUrls: ['./color-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ColorRangePanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts index 85e248e811..0c0022b252 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts @@ -58,16 +58,17 @@ export class ColorRangeSettingsComponentService { } @Component({ - selector: 'tb-color-range-settings', - templateUrl: './color-range-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ColorRangeSettingsComponent), - multi: true - } - ] + selector: 'tb-color-range-settings', + templateUrl: './color-range-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ColorRangeSettingsComponent), + multi: true + } + ], + standalone: false }) export class ColorRangeSettingsComponent implements OnInit, ControlValueAccessor, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts index 7b34cc5967..ecbb03b580 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts @@ -36,11 +36,12 @@ import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/k import { Datasource } from '@shared/models/widget.models'; @Component({ - selector: 'tb-color-settings-panel', - templateUrl: './color-settings-panel.component.html', - providers: [], - styleUrls: ['./color-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-color-settings-panel', + templateUrl: './color-settings-panel.component.html', + providers: [], + styleUrls: ['./color-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ColorSettingsPanelComponent extends PageComponent implements OnInit { 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 811a0a4f60..e4744b8918 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 @@ -65,16 +65,17 @@ export class ColorSettingsComponentService { } @Component({ - selector: 'tb-color-settings', - templateUrl: './color-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ColorSettingsComponent), - multi: true - } - ] + selector: 'tb-color-settings', + templateUrl: './color-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ColorSettingsComponent), + multi: true + } + ], + standalone: false }) export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts index dfa39e58c5..b78da31004 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts @@ -41,16 +41,17 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-count-widget-settings', - templateUrl: './count-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CountWidgetSettingsComponent), - multi: true - } - ] + selector: 'tb-count-widget-settings', + templateUrl: './count-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CountWidgetSettingsComponent), + multi: true + } + ], + standalone: false }) export class CountWidgetSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.ts index 41361112f5..5dc81ff163 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.ts @@ -31,21 +31,22 @@ import { isDefinedAndNotNull } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-css-size-input', - templateUrl: './css-size-input.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CssSizeInputComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CssSizeInputComponent), - multi: true, - } - ] + selector: 'tb-css-size-input', + templateUrl: './css-size-input.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CssSizeInputComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CssSizeInputComponent), + multi: true, + } + ], + standalone: false }) export class CssSizeInputComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts index 1d5ab7f12e..861bf64044 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts @@ -21,16 +21,17 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-css-unit-select', - templateUrl: './css-unit-select.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CssUnitSelectComponent), - multi: true - } - ] + selector: 'tb-css-unit-select', + templateUrl: './css-unit-select.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CssUnitSelectComponent), + multi: true + } + ], + standalone: false }) export class CssUnitSelectComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts index 9545e6cb37..746cc6c90b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts @@ -48,16 +48,17 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-date-format-select', - templateUrl: './date-format-select.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DateFormatSelectComponent), - multi: true - } - ] + selector: 'tb-date-format-select', + templateUrl: './date-format-select.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DateFormatSelectComponent), + multi: true + } + ], + standalone: false }) export class DateFormatSelectComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts index 45ffdcc580..b66d49c711 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts @@ -25,11 +25,12 @@ import { DatePipe } from '@angular/common'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-date-format-settings-panel', - templateUrl: './date-format-settings-panel.component.html', - providers: [], - styleUrls: ['./date-format-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-date-format-settings-panel', + templateUrl: './date-format-settings-panel.component.html', + providers: [], + styleUrls: ['./date-format-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DateFormatSettingsPanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts index f5b71fb52c..f0b085dbae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts @@ -31,22 +31,23 @@ import { defaultFormPropertyValue, FormProperty } from '@shared/models/dynamic-f import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ - selector: 'tb-dynamic-form-array', - templateUrl: './dynamic-form-array.component.html', - styleUrls: ['./dynamic-form-array.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DynamicFormArrayComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DynamicFormArrayComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-dynamic-form-array', + templateUrl: './dynamic-form-array.component.html', + styleUrls: ['./dynamic-form-array.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFormArrayComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DynamicFormArrayComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DynamicFormArrayComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts index bddbda4401..d994a65901 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts @@ -53,22 +53,23 @@ import { DialogService } from '@core/services/dialog.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-dynamic-form-properties', - templateUrl: './dynamic-form-properties.component.html', - styleUrls: ['./dynamic-form-properties.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DynamicFormPropertiesComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DynamicFormPropertiesComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-dynamic-form-properties', + templateUrl: './dynamic-form-properties.component.html', + styleUrls: ['./dynamic-form-properties.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFormPropertiesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DynamicFormPropertiesComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts index dfcdf49192..e59a08037f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts @@ -34,10 +34,11 @@ import { isUndefinedOrNull } from '@core/utils'; import { StringItemsOption } from '@shared/components/string-items-list.component'; @Component({ - selector: 'tb-dynamic-form-property-panel', - templateUrl: './dynamic-form-property-panel.component.html', - styleUrls: ['./dynamic-form-property-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-dynamic-form-property-panel', + templateUrl: './dynamic-form-property-panel.component.html', + styleUrls: ['./dynamic-form-property-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DynamicFormPropertyPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts index 7692be80e0..4f1dd8b23d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts @@ -59,22 +59,23 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-dynamic-form-property-row', - templateUrl: './dynamic-form-property-row.component.html', - styleUrls: ['./dynamic-form-property-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DynamicFormPropertyRowComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DynamicFormPropertyRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-dynamic-form-property-row', + templateUrl: './dynamic-form-property-row.component.html', + styleUrls: ['./dynamic-form-property-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFormPropertyRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DynamicFormPropertyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DynamicFormPropertyRowComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts index 6d414a18f9..d92cc58489 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts @@ -46,22 +46,23 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export const selectItemValid = (item: FormSelectItem): boolean => isDefinedAndNotNull(item.value) && !!item.label; @Component({ - selector: 'tb-dynamic-form-select-item-row', - templateUrl: './dynamic-form-select-item-row.component.html', - styleUrls: ['./dynamic-form-select-item-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DynamicFormSelectItemRowComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DynamicFormSelectItemRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-dynamic-form-select-item-row', + templateUrl: './dynamic-form-select-item-row.component.html', + styleUrls: ['./dynamic-form-select-item-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFormSelectItemRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DynamicFormSelectItemRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DynamicFormSelectItemRowComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts index 9d664d5796..8970569cf3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts @@ -45,22 +45,23 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-dynamic-form-select-items', - templateUrl: './dynamic-form-select-items.component.html', - styleUrls: ['./dynamic-form-select-items.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DynamicFormSelectItemsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DynamicFormSelectItemsComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-dynamic-form-select-items', + templateUrl: './dynamic-form-select-items.component.html', + styleUrls: ['./dynamic-form-select-items.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFormSelectItemsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DynamicFormSelectItemsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DynamicFormSelectItemsComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts index 7441bf470f..5fa33a422b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts @@ -55,21 +55,22 @@ import { ContentType } from '@shared/models/constants'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ - selector: 'tb-dynamic-form', - templateUrl: './dynamic-form.component.html', - styleUrls: ['./dynamic-form.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DynamicFormComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DynamicFormComponent), - multi: true - } - ] + selector: 'tb-dynamic-form', + templateUrl: './dynamic-form.component.html', + styleUrls: ['./dynamic-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFormComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DynamicFormComponent), + multi: true + } + ], + standalone: false }) export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.ts index f774a32657..95f137ce39 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.ts @@ -39,17 +39,18 @@ import { Observable, of } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-entity-alias-input', - templateUrl: './entity-alias-input.component.html', - styleUrls: ['./entity-alias-input.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EntityAliasInputComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-entity-alias-input', + templateUrl: './entity-alias-input.component.html', + styleUrls: ['./entity-alias-input.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAliasInputComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class EntityAliasInputComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts index 80e1ce105b..c57ce2a231 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts @@ -37,18 +37,19 @@ import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form- import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-filter-select', - templateUrl: './filter-select.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FilterSelectComponent), - multi: true - }, - { - provide: ErrorStateMatcher, - useExisting: FilterSelectComponent - }] + selector: 'tb-filter-select', + templateUrl: './filter-select.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterSelectComponent), + multi: true + }, + { + provide: ErrorStateMatcher, + useExisting: FilterSelectComponent + }], + standalone: false }) export class FilterSelectComponent implements ControlValueAccessor, OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts index 07d9173fd8..7428439921 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts @@ -46,11 +46,12 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-font-settings-panel', - templateUrl: './font-settings-panel.component.html', - providers: [], - styleUrls: ['./font-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-font-settings-panel', + templateUrl: './font-settings-panel.component.html', + providers: [], + styleUrls: ['./font-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class FontSettingsPanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts index 203b9080bf..52e2169311 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts @@ -24,16 +24,17 @@ import { isDefinedAndNotNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-font-settings', - templateUrl: './font-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FontSettingsComponent), - multi: true - } - ] + selector: 'tb-font-settings', + templateUrl: './font-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FontSettingsComponent), + multi: true + } + ], + standalone: false }) export class FontSettingsComponent implements OnInit, ControlValueAccessor { 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 633ae1b89d..fa97267843 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 @@ -37,16 +37,17 @@ import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/k import { Datasource } from '@shared/models/widget.models'; @Component({ - selector: 'tb-gradient', - templateUrl: './gradient.component.html', - styleUrls: ['color-settings-panel.component.scss', 'gradient.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => GradientComponent), - multi: true - } - ] + selector: 'tb-gradient', + templateUrl: './gradient.component.html', + styleUrls: ['color-settings-panel.component.scss', 'gradient.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GradientComponent), + multi: true + } + ], + standalone: false }) export class GradientComponent implements OnInit, ControlValueAccessor, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts index dca656ac46..8021f9d2dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts @@ -52,7 +52,8 @@ export interface ImageCardsColumns { { // eslint-disable-next-line @angular-eslint/directive-selector selector: 'tb-image-cards-select-option', - } + standalone: false +} ) export class ImageCardsSelectOptionDirective { @@ -70,17 +71,18 @@ export class ImageCardsSelectOptionDirective { } @Component({ - selector: 'tb-image-cards-select', - templateUrl: './image-cards-select.component.html', - styleUrls: ['./image-cards-select.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ImageCardsSelectComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-image-cards-select', + templateUrl: './image-cards-select.component.html', + styleUrls: ['./image-cards-select.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ImageCardsSelectComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit, OnChanges, AfterContentInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts index 5e8f7e2ce6..b3caf03e89 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts @@ -25,16 +25,17 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { widgetTitleAutocompleteValues } from '@app/shared/public-api'; @Component({ - selector: 'tb-status-widget-state-settings', - templateUrl: './status-widget-state-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => StatusWidgetStateSettingsComponent), - multi: true - } - ] + selector: 'tb-status-widget-state-settings', + templateUrl: './status-widget-state-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => StatusWidgetStateSettingsComponent), + multi: true + } + ], + standalone: false }) export class StatusWidgetStateSettingsComponent implements OnInit, OnChanges, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts index 72329d208e..ba5725de08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts @@ -61,10 +61,11 @@ export interface DataKeyConfigDialogData { } @Component({ - selector: 'tb-data-key-config-dialog', - templateUrl: './data-key-config-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: DataKeyConfigDialogComponent}], - styleUrls: ['./data-key-config-dialog.component.scss'] + selector: 'tb-data-key-config-dialog', + templateUrl: './data-key-config-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: DataKeyConfigDialogComponent }], + styleUrls: ['./data-key-config-dialog.component.scss'], + standalone: false }) export class DataKeyConfigDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts index ac6596576c..87e25f2614 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts @@ -62,21 +62,22 @@ import { FormProperty } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-data-key-config', - templateUrl: './data-key-config.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataKeyConfigComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DataKeyConfigComponent), - multi: true, - } - ] + selector: 'tb-data-key-config', + templateUrl: './data-key-config.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeyConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataKeyConfigComponent), + multi: true, + } + ], + standalone: false }) export class DataKeyConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.ts index 1c1fa01956..28a6714d23 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.ts @@ -55,17 +55,18 @@ import { IAliasController } from '@core/api/widget-api.models'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; @Component({ - selector: 'tb-data-key-input', - templateUrl: './data-key-input.component.html', - styleUrls: ['./data-key-input.component.scss', './data-keys.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataKeyInputComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-data-key-input', + templateUrl: './data-key-input.component.html', + styleUrls: ['./data-key-input.component.scss', './data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeyInputComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DataKeyInputComponent implements ControlValueAccessor, OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts index 44fb225c96..02aa5b7548 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts @@ -75,26 +75,27 @@ import { FormProperty } from '@shared/models/dynamic-form.models'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; @Component({ - selector: 'tb-data-keys', - templateUrl: './data-keys.component.html', - styleUrls: ['./data-keys.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataKeysComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DataKeysComponent), - multi: true, - }, - { - provide: ErrorStateMatcher, - useExisting: DataKeysComponent - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-data-keys', + templateUrl: './data-keys.component.html', + styleUrls: ['./data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeysComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataKeysComponent), + multi: true, + }, + { + provide: ErrorStateMatcher, + useExisting: DataKeysComponent + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChanges, ErrorStateMatcher, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts index 0f946cd479..99e7f21e19 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts @@ -29,16 +29,17 @@ import { coerceBoolean } from '@shared/decorators/coercion'; // @dynamic @Component({ - selector: 'tb-legend-config', - templateUrl: './legend-config.component.html', - styleUrls: ['./../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => LegendConfigComponent), - multi: true - } - ] + selector: 'tb-legend-config', + templateUrl: './legend-config.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LegendConfigComponent), + multi: true + } + ], + standalone: false }) export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts index 57646cb304..7976a3773a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts @@ -41,17 +41,18 @@ import { genNextLabelForDataKeys } from '@core/utils'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ - selector: 'tb-additional-map-data-source-row', - templateUrl: './additional-map-data-source-row.component.html', - styleUrls: ['./additional-map-data-source-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AdditionalMapDataSourceRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-additional-map-data-source-row', + templateUrl: './additional-map-data-source-row.component.html', + styleUrls: ['./additional-map-data-source-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AdditionalMapDataSourceRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class AdditionalMapDataSourceRowComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.ts index 7e96dcf847..eb8fef90d0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.ts @@ -37,22 +37,23 @@ import { import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ - selector: 'tb-additional-map-data-sources', - templateUrl: './additional-map-data-sources.component.html', - styleUrls: ['./additional-map-data-sources.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AdditionalMapDataSourcesComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => AdditionalMapDataSourcesComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-additional-map-data-sources', + templateUrl: './additional-map-data-sources.component.html', + styleUrls: ['./additional-map-data-sources.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AdditionalMapDataSourcesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AdditionalMapDataSourcesComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class AdditionalMapDataSourcesComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts index 7f8fe16279..102cfe97b8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts @@ -28,11 +28,12 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ - selector: 'tb-data-layer-color-settings-panel', - templateUrl: './data-layer-color-settings-panel.component.html', - providers: [], - styleUrls: ['./data-layer-color-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-data-layer-color-settings-panel', + templateUrl: './data-layer-color-settings-panel.component.html', + providers: [], + styleUrls: ['./data-layer-color-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DataLayerColorSettingsPanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts index 17e3a15177..f3c095ba4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts @@ -27,16 +27,17 @@ import { MapSettingsContext } from '@home/components/widget/lib/settings/common/ import { DatasourceType } from '@shared/models/widget.models'; @Component({ - selector: 'tb-data-layer-color-settings', - templateUrl: './data-layer-color-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataLayerColorSettingsComponent), - multi: true - } - ] + selector: 'tb-data-layer-color-settings', + templateUrl: './data-layer-color-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataLayerColorSettingsComponent), + multi: true + } + ], + standalone: false }) export class DataLayerColorSettingsComponent implements ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts index 5a7c1afa0b..6bc2fb7489 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts @@ -37,21 +37,22 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ - selector: 'tb-data-layer-pattern-settings', - templateUrl: './data-layer-pattern-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataLayerPatternSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DataLayerPatternSettingsComponent), - multi: true - } - ] + selector: 'tb-data-layer-pattern-settings', + templateUrl: './data-layer-pattern-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataLayerPatternSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataLayerPatternSettingsComponent), + multi: true + } + ], + standalone: false }) export class DataLayerPatternSettingsComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts index ddd3b1842d..ed02417950 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.ts @@ -32,21 +32,22 @@ import { MapSettingsContext } from '@home/components/widget/lib/settings/common/ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @Component({ - selector: 'tb-image-map-source-settings', - templateUrl: './image-map-source-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ImageMapSourceSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ImageMapSourceSettingsComponent), - multi: true - } - ] + selector: 'tb-image-map-source-settings', + templateUrl: './image-map-source-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ImageMapSourceSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ImageMapSourceSettingsComponent), + multi: true + } + ], + standalone: false }) export class ImageMapSourceSettingsComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts index b58999a406..3d474c79e1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts @@ -30,18 +30,19 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { isEmptyStr } from '@core/utils'; @Component({ - selector: 'tb-map-action-button-row', - templateUrl: 'map-action-button-row.component.html', - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapActionButtonRowComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MapActionButtonRowComponent), - multi: true - }] + selector: 'tb-map-action-button-row', + templateUrl: 'map-action-button-row.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapActionButtonRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapActionButtonRowComponent), + multi: true + }], + standalone: false }) export class MapActionButtonRowComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts index 76968e3e4d..2494d4b30a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts @@ -31,18 +31,19 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-map-action-button-settings', - templateUrl: './map-action-buttons-settings.component.html', - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapActionButtonsSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MapActionButtonsSettingsComponent), - multi: true - }] + selector: 'tb-map-action-button-settings', + templateUrl: './map-action-buttons-settings.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapActionButtonsSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapActionButtonsSettingsComponent), + multi: true + }], + standalone: false }) export class MapActionButtonsSettingsComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 62a46165dd..670e5c4600 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -56,10 +56,11 @@ export interface MapDataLayerDialogData { } @Component({ - selector: 'tb-map-data-layer-dialog', - templateUrl: './map-data-layer-dialog.component.html', - styleUrls: ['./map-data-layer-dialog.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-data-layer-dialog', + templateUrl: './map-data-layer-dialog.component.html', + styleUrls: ['./map-data-layer-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapDataLayerDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts index 318e567219..3b482d32cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts @@ -57,17 +57,18 @@ import { import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ - selector: 'tb-map-data-layer-row', - templateUrl: './map-data-layer-row.component.html', - styleUrls: ['./map-data-layer-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapDataLayerRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-data-layer-row', + templateUrl: './map-data-layer-row.component.html', + styleUrls: ['./map-data-layer-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapDataLayerRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts index b11d836cfa..b8e1b73115 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts @@ -41,22 +41,23 @@ import { MapSettingsContext } from '@home/components/widget/lib/settings/common/ import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ - selector: 'tb-map-data-layers', - templateUrl: './map-data-layers.component.html', - styleUrls: ['./map-data-layers.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapDataLayersComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MapDataLayersComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-data-layers', + templateUrl: './map-data-layers.component.html', + styleUrls: ['./map-data-layers.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapDataLayersComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapDataLayersComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts index 7737c3a4b6..114e22b5a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts @@ -39,17 +39,18 @@ import { EntityType } from '@shared/models/entity-type.models'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ - selector: 'tb-map-data-source-row', - templateUrl: './map-data-source-row.component.html', - styleUrls: ['./map-data-source-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapDataSourceRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-data-source-row', + templateUrl: './map-data-source-row.component.html', + styleUrls: ['./map-data-source-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapDataSourceRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapDataSourceRowComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts index 4b62112cc1..71afc02dec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts @@ -37,22 +37,23 @@ import { import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ - selector: 'tb-map-data-sources', - templateUrl: './map-data-sources.component.html', - styleUrls: ['./map-data-sources.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapDataSourcesComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MapDataSourcesComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-data-sources', + templateUrl: './map-data-sources.component.html', + styleUrls: ['./map-data-sources.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapDataSourcesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapDataSourcesComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapDataSourcesComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts index 14136f018e..1604ff8683 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts @@ -60,17 +60,18 @@ import { } from '@home/components/widget/lib/settings/common/map/map-layer-settings-panel.component'; @Component({ - selector: 'tb-map-layer-row', - templateUrl: './map-layer-row.component.html', - styleUrls: ['./map-layer-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapLayerRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-layer-row', + templateUrl: './map-layer-row.component.html', + styleUrls: ['./map-layer-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapLayerRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapLayerRowComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts index 536c953bb7..bedb92a6af 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts @@ -37,11 +37,12 @@ import { import { TranslateService } from '@ngx-translate/core'; @Component({ - selector: 'tb-map-layer-settings-panel', - templateUrl: './map-layer-settings-panel.component.html', - providers: [], - styleUrls: ['./map-layer-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-layer-settings-panel', + templateUrl: './map-layer-settings-panel.component.html', + providers: [], + styleUrls: ['./map-layer-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapLayerSettingsPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts index feaa633b34..35dc5ff04a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.ts @@ -38,22 +38,23 @@ import { } from '@shared/models/widget/maps/map.models'; @Component({ - selector: 'tb-map-layers', - templateUrl: './map-layers.component.html', - styleUrls: ['./map-layers.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapLayersComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MapLayersComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-layers', + templateUrl: './map-layers.component.html', + styleUrls: ['./map-layers.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapLayersComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapLayersComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapLayersComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 322fbdbde0..51b1fa271b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -54,21 +54,22 @@ import { deepClone, mergeDeep } from '@core/utils'; import { MatDialog } from '@angular/material/dialog'; @Component({ - selector: 'tb-map-settings', - templateUrl: './map-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MapSettingsComponent), - multi: true - } - ] + selector: 'tb-map-settings', + templateUrl: './map-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapSettingsComponent), + multi: true + } + ], + standalone: false }) export class MapSettingsComponent implements OnInit, ControlValueAccessor, Validator, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.ts index 5c751ac950..8c9864c456 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.ts @@ -37,17 +37,18 @@ import { TranslateService } from '@ngx-translate/core'; import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-map-tooltip-tag-actions-panel', - templateUrl: './map-tooltip-tag-actions.component.html', - styleUrls: ['./map-tooltip-tag-actions.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapTooltipTagActionsComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-map-tooltip-tag-actions-panel', + templateUrl: './map-tooltip-tag-actions.component.html', + styleUrls: ['./map-tooltip-tag-actions.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapTooltipTagActionsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MapTooltipTagActionsComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.ts index 8c7ff9371e..eac6b9e410 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-clustering-settings.component.ts @@ -31,21 +31,22 @@ import { MarkerClusteringSettings } from '@shared/models/widget/maps/map.models' import { merge } from 'rxjs'; @Component({ - selector: 'tb-marker-clustering-settings', - templateUrl: './marker-clustering-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MarkerClusteringSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MarkerClusteringSettingsComponent), - multi: true - } - ] + selector: 'tb-marker-clustering-settings', + templateUrl: './marker-clustering-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkerClusteringSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MarkerClusteringSettingsComponent), + multi: true + } + ], + standalone: false }) export class MarkerClusteringSettingsComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts index f1671807d8..8094bc0a68 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.ts @@ -42,11 +42,12 @@ interface MarkerIconContainerInfo { } @Component({ - selector: 'tb-marker-icon-shapes', - templateUrl: './marker-icon-shapes.component.html', - providers: [], - styleUrls: ['./marker-icon-shapes.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-marker-icon-shapes', + templateUrl: './marker-icon-shapes.component.html', + providers: [], + styleUrls: ['./marker-icon-shapes.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MarkerIconShapesComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts index eea2bc6700..67539b82f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.ts @@ -25,11 +25,12 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MarkerImageSettings, MarkerImageType } from '@shared/models/widget/maps/map.models'; @Component({ - selector: 'tb-marker-image-settings-panel', - templateUrl: './marker-image-settings-panel.component.html', - providers: [], - styleUrls: ['./marker-image-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-marker-image-settings-panel', + templateUrl: './marker-image-settings-panel.component.html', + providers: [], + styleUrls: ['./marker-image-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MarkerImageSettingsPanelComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts index 0d84469410..6da6c809e0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts @@ -24,16 +24,17 @@ import { } from '@home/components/widget/lib/settings/common/map/marker-image-settings-panel.component'; @Component({ - selector: 'tb-marker-image-settings', - templateUrl: './marker-image-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MarkerImageSettingsComponent), - multi: true - } - ] + selector: 'tb-marker-image-settings', + templateUrl: './marker-image-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkerImageSettingsComponent), + multi: true + } + ], + standalone: false }) export class MarkerImageSettingsComponent implements ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts index 319b423b2d..e251f6f9a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts @@ -53,16 +53,17 @@ import { MapSettingsContext } from '@home/components/widget/lib/settings/common/ import { DatasourceType } from '@shared/models/widget.models'; @Component({ - selector: 'tb-marker-shape-settings', - templateUrl: './marker-shape-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MarkerShapeSettingsComponent), - multi: true - } - ] + selector: 'tb-marker-shape-settings', + templateUrl: './marker-shape-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkerShapeSettingsComponent), + multi: true + } + ], + standalone: false }) export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts index 5499c6e39c..23fadbf093 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.ts @@ -37,11 +37,12 @@ interface MarkerShapeInfo { } @Component({ - selector: 'tb-marker-shapes', - templateUrl: './marker-shapes.component.html', - providers: [], - styleUrls: ['./marker-shapes.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-marker-shapes', + templateUrl: './marker-shapes.component.html', + providers: [], + styleUrls: ['./marker-shapes.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MarkerShapesComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts index 8988365644..dcea878241 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts @@ -22,11 +22,12 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ShapeFillImageSettings, ShapeFillImageType } from '@shared/models/widget/maps/map.models'; @Component({ - selector: 'tb-shape-fill-image-settings-panel', - templateUrl: './shape-fill-image-settings-panel.component.html', - providers: [], - styleUrls: ['./shape-fill-image-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-shape-fill-image-settings-panel', + templateUrl: './shape-fill-image-settings-panel.component.html', + providers: [], + styleUrls: ['./shape-fill-image-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ShapeFillImageSettingsPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts index 466525f46f..5c9bbb260f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts @@ -24,16 +24,17 @@ import { } from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component'; @Component({ - selector: 'tb-shape-fill-image-settings', - templateUrl: './shape-fill-image-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ShapeFillImageSettingsComponent), - multi: true - } - ] + selector: 'tb-shape-fill-image-settings', + templateUrl: './shape-fill-image-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ShapeFillImageSettingsComponent), + multi: true + } + ], + standalone: false }) export class ShapeFillImageSettingsComponent implements ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts index 60fab91c82..52ab290e6f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts @@ -28,11 +28,12 @@ import { MapSettingsContext } from '@home/components/widget/lib/settings/common/ import { DatasourceType } from '@shared/models/widget.models'; @Component({ - selector: 'tb-shape-fill-stripe-settings-panel', - templateUrl: './shape-fill-stripe-settings-panel.component.html', - providers: [], - styleUrls: ['./shape-fill-stripe-settings-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-shape-fill-stripe-settings-panel', + templateUrl: './shape-fill-stripe-settings-panel.component.html', + providers: [], + styleUrls: ['./shape-fill-stripe-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ShapeFillStripeSettingsPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts index 0b8d19b4a7..b1f65c541c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts @@ -28,16 +28,17 @@ import { } from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; @Component({ - selector: 'tb-shape-fill-stripe-settings', - templateUrl: './shape-fill-stripe-settings.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ShapeFillStripeSettingsComponent), - multi: true - } - ] + selector: 'tb-shape-fill-stripe-settings', + templateUrl: './shape-fill-stripe-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ShapeFillStripeSettingsComponent), + multi: true + } + ], + standalone: false }) export class ShapeFillStripeSettingsComponent implements ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts index 1a041cd584..b3064b0dd3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.ts @@ -33,21 +33,22 @@ import { } from '@shared/models/widget/maps/map.models'; @Component({ - selector: 'tb-trip-timeline-settings', - templateUrl: './trip-timeline-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TripTimelineSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TripTimelineSettingsComponent), - multi: true - } - ] + selector: 'tb-trip-timeline-settings', + templateUrl: './trip-timeline-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripTimelineSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripTimelineSettingsComponent), + multi: true + } + ], + standalone: false }) export class TripTimelineSettingsComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts index 80d31cf678..90616286f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts @@ -56,21 +56,22 @@ import { map } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-scada-symbol-object-settings', - templateUrl: './scada-symbol-object-settings.component.html', - styleUrls: ['./scada-symbol-object-settings.component.scss', './../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ScadaSymbolObjectSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ScadaSymbolObjectSettingsComponent), - multi: true - } - ] + selector: 'tb-scada-symbol-object-settings', + templateUrl: './scada-symbol-object-settings.component.html', + styleUrls: ['./scada-symbol-object-settings.component.scss', './../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolObjectSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolObjectSettingsComponent), + multi: true + } + ], + standalone: false }) export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts index 2bc5f55597..cabeb765c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts @@ -39,16 +39,17 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-value-source-data-key', - templateUrl: './value-source-data-key.component.html', - styleUrls: ['value-source-data-key.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ValueSourceDataKeyComponent), - multi: true - } - ] + selector: 'tb-value-source-data-key', + templateUrl: './value-source-data-key.component.html', + styleUrls: ['value-source-data-key.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ValueSourceDataKeyComponent), + multi: true + } + ], + standalone: false }) export class ValueSourceDataKeyComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts index e006e54208..2b91efa474 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts @@ -37,16 +37,17 @@ export interface ValueSourceProperty { } @Component({ - selector: 'tb-value-source', - templateUrl: './value-source.component.html', - styleUrls: ['./../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ValueSourceComponent), - multi: true - } - ] + selector: 'tb-value-source', + templateUrl: './value-source.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ValueSourceComponent), + multi: true + } + ], + standalone: false }) export class ValueSourceComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.ts index 980cfe5780..79633ac7f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.ts @@ -32,16 +32,17 @@ export interface WidgetFont { } @Component({ - selector: 'tb-widget-font', - templateUrl: './widget-font.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetFontComponent), - multi: true - } - ] + selector: 'tb-widget-font', + templateUrl: './widget-font.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetFontComponent), + multi: true + } + ], + standalone: false }) export class WidgetFontComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.ts index 7b07fe47e0..d779a634ac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.ts @@ -47,19 +47,20 @@ import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-con import { FormProperty } from '@shared/models/dynamic-form.models'; @Component({ - selector: 'tb-widget-settings', - templateUrl: './widget-settings.component.html', - styleUrls: ['./widget-settings.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => WidgetSettingsComponent), - multi: true - }] + selector: 'tb-widget-settings', + templateUrl: './widget-settings.component.html', + styleUrls: ['./widget-settings.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => WidgetSettingsComponent), + multi: true + }], + standalone: false }) export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy, OnChanges, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts index 52110ad50a..f02de5fa8d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts @@ -49,16 +49,17 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device-key-autocomplete', - templateUrl: './device-key-autocomplete.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceKeyAutocompleteComponent), - multi: true - } - ] + selector: 'tb-device-key-autocomplete', + templateUrl: './device-key-autocomplete.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceKeyAutocompleteComponent), + multi: true + } + ], + standalone: false }) export class DeviceKeyAutocompleteComponent extends PageComponent implements OnInit, ControlValueAccessor, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts index 481097cf94..9c115025d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts @@ -24,9 +24,10 @@ import { ValueType } from '@shared/models/constants'; import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-knob-control-widget-settings', - templateUrl: './knob-control-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-knob-control-widget-settings', + templateUrl: './knob-control-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class KnobControlWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts index 45cfbcf7ec..faaa56db65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts @@ -23,9 +23,10 @@ import { WidgetService } from '@core/http/widget.service'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @Component({ - selector: 'tb-led-indicator-widget-settings', - templateUrl: './led-indicator-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-led-indicator-widget-settings', + templateUrl: './led-indicator-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class LedIndicatorWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts index d89e46e7b1..170947af25 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts @@ -35,9 +35,10 @@ interface DisplayColumn { } @Component({ - selector: 'tb-persistent-table-widget-settings', - templateUrl: './persistent-table-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-persistent-table-widget-settings', + templateUrl: './persistent-table-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class PersistentTableWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts index 6f008e5a82..8c443e1462 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts @@ -23,9 +23,10 @@ import { switchRpcDefaultSettings } from '@home/components/widget/lib/settings/c import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-round-switch-widget-settings', - templateUrl: './round-switch-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-round-switch-widget-settings', + templateUrl: './round-switch-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class RoundSwitchWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts index 6edd8f191b..730bf1a0b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts @@ -29,16 +29,17 @@ export interface RpcButtonStyle { } @Component({ - selector: 'tb-rpc-button-style', - templateUrl: './rpc-button-style.component.html', - styleUrls: ['./../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RpcButtonStyleComponent), - multi: true - } - ] + selector: 'tb-rpc-button-style', + templateUrl: './rpc-button-style.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RpcButtonStyleComponent), + multi: true + } + ], + standalone: false }) export class RpcButtonStyleComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts index 157689a4ad..141ac5a6de 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-rpc-shell-widget-settings', - templateUrl: './rpc-shell-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-rpc-shell-widget-settings', + templateUrl: './rpc-shell-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class RpcShellWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts index 77e60522fe..31a5a0d81b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-rpc-terminal-widget-settings', - templateUrl: './rpc-terminal-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-rpc-terminal-widget-settings', + templateUrl: './rpc-terminal-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class RpcTerminalWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts index b8af3eaf0d..4319b823c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { ContentType } from '@shared/models/constants'; @Component({ - selector: 'tb-send-rpc-widget-settings', - templateUrl: './send-rpc-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-send-rpc-widget-settings', + templateUrl: './send-rpc-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class SendRpcWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts index d841317283..73124033e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts @@ -28,9 +28,10 @@ import { import { ValueType } from '@shared/models/constants'; @Component({ - selector: 'tb-single-switch-widget-settings', - templateUrl: './single-switch-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-single-switch-widget-settings', + templateUrl: './single-switch-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts index 5090e8c94f..d8d95bdddc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts @@ -23,9 +23,10 @@ import { switchRpcDefaultSettings } from '@home/components/widget/lib/settings/c import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-slide-toggle-widget-settings', - templateUrl: './slide-toggle-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-slide-toggle-widget-settings', + templateUrl: './slide-toggle-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.ts index 53a9d483a7..f3899d7c66 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.ts @@ -31,9 +31,10 @@ import { formatValue } from '@core/utils'; import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-slider-widget-settings', - templateUrl: './slider-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-slider-widget-settings', + templateUrl: './slider-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class SliderWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts index d51c9f8d8e..c9ea433d93 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts @@ -23,9 +23,10 @@ import { switchRpcDefaultSettings } from '@home/components/widget/lib/settings/c import { deepClone } from '@core/utils'; @Component({ - selector: 'tb-switch-control-widget-settings', - templateUrl: './switch-control-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-switch-control-widget-settings', + templateUrl: './switch-control-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts index e58006d1cf..29f436c61d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts @@ -65,21 +65,22 @@ export const switchRpcDefaultSettings = (): SwitchRpcSettings => ({ }); @Component({ - selector: 'tb-switch-rpc-settings', - templateUrl: './switch-rpc-settings.component.html', - styleUrls: ['./../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SwitchRpcSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => SwitchRpcSettingsComponent), - multi: true - } - ] + selector: 'tb-switch-rpc-settings', + templateUrl: './switch-rpc-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SwitchRpcSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SwitchRpcSettingsComponent), + multi: true + } + ], + standalone: false }) export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts index 765890a101..a76016b1e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { ContentType } from '@shared/models/constants'; @Component({ - selector: 'tb-update-device-attribute-widget-settings', - templateUrl: './update-device-attribute-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-device-attribute-widget-settings', + templateUrl: './update-device-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateDeviceAttributeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts index 72377dddbc..ac3921f3ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts @@ -33,9 +33,10 @@ import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; type ButtonAppearanceType = 'left' | 'right'; @Component({ - selector: 'tb-value-stepper-widget-settings', - templateUrl: './value-stepper-widget-settings.component.html', - styleUrls: ['../widget-settings.scss'] + selector: 'tb-value-stepper-widget-settings', + templateUrl: './value-stepper-widget-settings.component.html', + styleUrls: ['../widget-settings.scss'], + standalone: false }) export class ValueStepperWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.ts index ece9e5f9dc..183009aef7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-date-range-navigator-widget-settings', - templateUrl: './date-range-navigator-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-date-range-navigator-widget-settings', + templateUrl: './date-range-navigator-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class DateRangeNavigatorWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-hierarchy-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-hierarchy-widget-settings.component.ts index a990e234bb..3acf41af24 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-hierarchy-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-hierarchy-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-entities-hierarchy-widget-settings', - templateUrl: './entities-hierarchy-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-entities-hierarchy-widget-settings', + templateUrl: './entities-hierarchy-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class EntitiesHierarchyWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.ts index 22483166f0..c1c4a5eab3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-entities-table-key-settings', - templateUrl: './entities-table-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-entities-table-key-settings', + templateUrl: './entities-table-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class EntitiesTableKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.ts index e4238a13d2..68a560a956 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models'; @Component({ - selector: 'tb-entities-table-widget-settings', - templateUrl: './entities-table-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-entities-table-widget-settings', + templateUrl: './entities-table-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class EntitiesTableWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.ts index e8bc5f9bad..8307aaff1d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entity-count-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { countDefaultSettings } from '@home/components/widget/lib/count/count-widget.models'; @Component({ - selector: 'tb-entity-count-widget-settings', - templateUrl: './entity-count-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-entity-count-widget-settings', + templateUrl: './entity-count-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class EntityCountWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts index 60035457d1..9b91652207 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-gateway-config-single-device-widget-settings', - templateUrl: './gateway-config-single-device-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-gateway-config-single-device-widget-settings', + templateUrl: './gateway-config-single-device-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class GatewayConfigSingleDeviceWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts index d2d921db68..0eef25e44d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-gateway-config-widget-settings', - templateUrl: './gateway-config-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-gateway-config-widget-settings', + templateUrl: './gateway-config-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class GatewayConfigWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.ts index ef7e8c7574..a51c58a4c9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.ts @@ -23,9 +23,10 @@ import { MatChipInputEvent } from '@angular/material/chips'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; @Component({ - selector: 'tb-gateway-events-widget-settings', - templateUrl: './gateway-events-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-gateway-events-widget-settings', + templateUrl: './gateway-events-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class GatewayEventsWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts index 9af2f702da..95d7b437da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-gateway-logs-settings', - templateUrl: './gateway-logs-settings.component.html', - styleUrls: ['../widget-settings.scss'] + selector: 'tb-gateway-logs-settings', + templateUrl: './gateway-logs-settings.component.html', + styleUrls: ['../widget-settings.scss'], + standalone: false }) export class GatewayLogsSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts index 720b2538a2..9e30608b3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-gateway-service-rpc-settings', - templateUrl: './gateway-service-rpc-settings.component.html', - styleUrls: ['../widget-settings.scss'] + selector: 'tb-gateway-service-rpc-settings', + templateUrl: './gateway-service-rpc-settings.component.html', + styleUrls: ['../widget-settings.scss'], + standalone: false }) export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.ts index 625f205507..40331412b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.ts @@ -22,9 +22,10 @@ import { Component } from '@angular/core'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; @Component({ - selector: 'tb-analogue-compass-widget-settings', - templateUrl: './analogue-compass-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-analogue-compass-widget-settings', + templateUrl: './analogue-compass-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class AnalogueCompassWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-linear-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-linear-gauge-widget-settings.component.ts index ef7589de8c..58741c3350 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-linear-gauge-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-linear-gauge-widget-settings.component.ts @@ -24,9 +24,10 @@ import { } from '@home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component'; @Component({ - selector: 'tb-analogue-linear-gauge-widget-settings', - templateUrl: './analogue-gauge-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-analogue-linear-gauge-widget-settings', + templateUrl: './analogue-gauge-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class AnalogueLinearGaugeWidgetSettingsComponent extends AnalogueGaugeWidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-radial-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-radial-gauge-widget-settings.component.ts index 891e248d67..6b60cf8ac0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-radial-gauge-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-radial-gauge-widget-settings.component.ts @@ -24,9 +24,10 @@ import { } from '@home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component'; @Component({ - selector: 'tb-analogue-radial-gauge-widget-settings', - templateUrl: './analogue-gauge-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-analogue-radial-gauge-widget-settings', + templateUrl: './analogue-gauge-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class AnalogueRadialGaugeWidgetSettingsComponent extends AnalogueGaugeWidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts index e55bbeef66..ff22abd28a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts @@ -49,9 +49,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-digital-gauge-widget-settings', - templateUrl: './digital-gauge-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-digital-gauge-widget-settings', + templateUrl: './digital-gauge-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts index dbc093a11f..82e8aacd1f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts @@ -27,16 +27,17 @@ import { Datasource } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-tick-value', - templateUrl: './tick-value.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TickValueComponent), - multi: true - } - ] + selector: 'tb-tick-value', + templateUrl: './tick-value.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TickValueComponent), + multi: true + } + ], + standalone: false }) export class TickValueComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.ts index a65f85e500..059c027310 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.ts @@ -23,9 +23,10 @@ import { GpioItem, gpioItemValidator } from '@home/components/widget/lib/setting import { ContentType } from '@shared/models/constants'; @Component({ - selector: 'tb-gpio-control-widget-settings', - templateUrl: './gpio-control-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-gpio-control-widget-settings', + templateUrl: './gpio-control-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class GpioControlWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts index cae8737c3b..229fc61c6f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts @@ -55,16 +55,17 @@ export const gpioItemValidator = (hasColor: boolean): ValidatorFn => (control: A }; @Component({ - selector: 'tb-gpio-item', - templateUrl: './gpio-item.component.html', - styleUrls: ['./gpio-item.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => GpioItemComponent), - multi: true - } - ] + selector: 'tb-gpio-item', + templateUrl: './gpio-item.component.html', + styleUrls: ['./gpio-item.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GpioItemComponent), + multi: true + } + ], + standalone: false }) export class GpioItemComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.ts index 0a2c20f0bf..dff3f9c3cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { GpioItem, gpioItemValidator } from '@home/components/widget/lib/settings/gpio/gpio-item.component'; @Component({ - selector: 'tb-gpio-panel-widget-settings', - templateUrl: './gpio-panel-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-gpio-panel-widget-settings', + templateUrl: './gpio-panel-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class GpioPanelWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts index 7fdb10dc99..b57de685f6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/doc-links-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-doc-links-widget-settings', - templateUrl: './doc-links-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-doc-links-widget-settings', + templateUrl: './doc-links-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class DocLinksWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts index 7e90c21767..48806e14ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-quick-links-widget-settings', - templateUrl: './quick-links-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-quick-links-widget-settings', + templateUrl: './quick-links-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class QuickLinksWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts index 41b28a3b4c..abd51b35d5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts @@ -30,9 +30,10 @@ import { import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-battery-level-widget-settings', - templateUrl: './battery-level-widget-settings.component.html', - styleUrls: [] + selector: 'tb-battery-level-widget-settings', + templateUrl: './battery-level-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class BatteryLevelWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts index a0d0afc14d..f3086291d6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts @@ -56,9 +56,10 @@ import { EntityService } from '@core/http/entity.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-liquid-level-card-widget-settings', - templateUrl: './liquid-level-card-widget-settings.component.html', - styleUrls: [] + selector: 'tb-liquid-level-card-widget-settings', + templateUrl: './liquid-level-card-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/signal-strength-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/signal-strength-widget-settings.component.ts index a676a8279b..6ce541273a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/signal-strength-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/signal-strength-widget-settings.component.ts @@ -30,9 +30,10 @@ import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-s import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ - selector: 'tb-signal-strength-widget-settings', - templateUrl: './signal-strength-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'], + selector: 'tb-signal-strength-widget-settings', + templateUrl: './signal-strength-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class SignalStrengthWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts index b2327a94fa..942d2cf3c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts @@ -28,9 +28,10 @@ import { import { ValueType } from '@shared/models/constants'; @Component({ - selector: 'tb-status-widget-settings', - templateUrl: './status-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'], + selector: 'tb-status-widget-settings', + templateUrl: './status-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class StatusWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts index 63689be5c5..1734b25c46 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts @@ -45,16 +45,17 @@ export const dataKeySelectOptionValidator = (control: AbstractControl) => { }; @Component({ - selector: 'tb-datakey-select-option', - templateUrl: './datakey-select-option.component.html', - styleUrls: ['./datakey-select-option.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DataKeySelectOptionComponent), - multi: true - } - ] + selector: 'tb-datakey-select-option', + templateUrl: './datakey-select-option.component.html', + styleUrls: ['./datakey-select-option.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeySelectOptionComponent), + multi: true + } + ], + standalone: false }) export class DataKeySelectOptionComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.ts index a74152df79..00993a3b27 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-device-claiming-widget-settings', - templateUrl: './device-claiming-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-device-claiming-widget-settings', + templateUrl: './device-claiming-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class DeviceClaimingWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts index 097d09ad9b..60245faff4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts @@ -21,9 +21,10 @@ import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; @Component({ - selector: 'tb-photo-camera-input-widget-settings', - templateUrl: './photo-camera-input-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-photo-camera-input-widget-settings', + templateUrl: './photo-camera-input-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts index acfc9a8b59..f6d89abfd0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts @@ -55,21 +55,22 @@ export function updateAttributeGeneralDefaultSettings(hasLabelValue = true): Upd } @Component({ - selector: 'tb-update-attribute-general-settings', - templateUrl: './update-attribute-general-settings.component.html', - styleUrls: ['./../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => UpdateAttributeGeneralSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => UpdateAttributeGeneralSettingsComponent), - multi: true - } - ] + selector: 'tb-update-attribute-general-settings', + templateUrl: './update-attribute-general-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => UpdateAttributeGeneralSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => UpdateAttributeGeneralSettingsComponent), + multi: true + } + ], + standalone: false }) export class UpdateAttributeGeneralSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts index 3b4b6e682b..b1c9065214 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-update-boolean-attribute-widget-settings', - templateUrl: './update-boolean-attribute-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-boolean-attribute-widget-settings', + templateUrl: './update-boolean-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateBooleanAttributeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.ts index 0e18de2ee0..895e0affc0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.ts @@ -25,9 +25,10 @@ import { } from '@home/components/widget/lib/settings/input/update-attribute-general-settings.component'; @Component({ - selector: 'tb-update-date-attribute-widget-settings', - templateUrl: './update-date-attribute-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-date-attribute-widget-settings', + templateUrl: './update-date-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateDateAttributeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts index 305db7e982..1323f552aa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts @@ -25,9 +25,10 @@ import { } from '@home/components/widget/lib/settings/input/update-attribute-general-settings.component'; @Component({ - selector: 'tb-update-double-attribute-widget-settings', - templateUrl: './update-double-attribute-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-double-attribute-widget-settings', + templateUrl: './update-double-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateDoubleAttributeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts index 0bb0e0af1d..53f65e5f19 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-update-image-attribute-widget-settings', - templateUrl: './update-image-attribute-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-image-attribute-widget-settings', + templateUrl: './update-image-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateImageAttributeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts index 997b9d1805..3d6ad3fc8f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts @@ -25,9 +25,10 @@ import { } from '@home/components/widget/lib/settings/input/update-attribute-general-settings.component'; @Component({ - selector: 'tb-update-integer-attribute-widget-settings', - templateUrl: './update-integer-attribute-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-integer-attribute-widget-settings', + templateUrl: './update-integer-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateIntegerAttributeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts index 70d4c42350..3236b9a4b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-update-json-attribute-widget-settings', - templateUrl: './update-json-attribute-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-json-attribute-widget-settings', + templateUrl: './update-json-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateJsonAttributeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts index 020bb13358..043b5e4373 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-update-location-attribute-widget-settings', - templateUrl: './update-location-attribute-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-location-attribute-widget-settings', + templateUrl: './update-location-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateLocationAttributeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts index d29decfc04..c007c51ef8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts @@ -30,9 +30,10 @@ import { } from '@home/components/widget/lib/multiple-input-widget.component'; @Component({ - selector: 'tb-update-multiple-attributes-key-settings', - templateUrl: './update-multiple-attributes-key-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-multiple-attributes-key-settings', + templateUrl: './update-multiple-attributes-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts index aa31db2b0b..0a92babdfb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-update-multiple-attributes-widget-settings', - templateUrl: './update-multiple-attributes-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-multiple-attributes-widget-settings', + templateUrl: './update-multiple-attributes-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts index ca134d8c73..0eb561a5f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts @@ -25,9 +25,10 @@ import { } from '@home/components/widget/lib/settings/input/update-attribute-general-settings.component'; @Component({ - selector: 'tb-update-string-attribute-widget-settings', - templateUrl: './update-string-attribute-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-update-string-attribute-widget-settings', + templateUrl: './update-string-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class UpdateStringAttributeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.ts index 173901aa16..f229998584 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.ts @@ -39,21 +39,22 @@ import { Widget } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-circle-settings', - templateUrl: './circle-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CircleSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CircleSettingsComponent), - multi: true - } - ] + selector: 'tb-circle-settings', + templateUrl: './circle-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CircleSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CircleSettingsComponent), + multi: true + } + ], + standalone: false }) export class CircleSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/common-map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/common-map-settings.component.ts index 8c49152772..8702cf28f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/common-map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/common-map-settings.component.ts @@ -34,21 +34,22 @@ import { Widget } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-common-map-settings', - templateUrl: './common-map-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CommonMapSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CommonMapSettingsComponent), - multi: true - } - ] + selector: 'tb-common-map-settings', + templateUrl: './common-map-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CommonMapSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CommonMapSettingsComponent), + multi: true + } + ], + standalone: false }) export class CommonMapSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.ts index c0c1ef98d4..7f085e920f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.ts @@ -28,16 +28,17 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-datasources-key-autocomplete', - templateUrl: './datasources-key-autocomplete.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DatasourcesKeyAutocompleteComponent), - multi: true - } - ] + selector: 'tb-datasources-key-autocomplete', + templateUrl: './datasources-key-autocomplete.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatasourcesKeyAutocompleteComponent), + multi: true + } + ], + standalone: false }) export class DatasourcesKeyAutocompleteComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.ts index b9d54db5a6..673f7fc573 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.ts @@ -37,21 +37,22 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-google-map-provider-settings', - templateUrl: './google-map-provider-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => GoogleMapProviderSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => GoogleMapProviderSettingsComponent), - multi: true - } - ] + selector: 'tb-google-map-provider-settings', + templateUrl: './google-map-provider-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GoogleMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GoogleMapProviderSettingsComponent), + multi: true + } + ], + standalone: false }) export class GoogleMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.ts index e201586a63..40140fb1ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.ts @@ -38,21 +38,22 @@ import { isDefinedAndNotNull } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-here-map-provider-settings', - templateUrl: './here-map-provider-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => HereMapProviderSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => HereMapProviderSettingsComponent), - multi: true - } - ] + selector: 'tb-here-map-provider-settings', + templateUrl: './here-map-provider-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => HereMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => HereMapProviderSettingsComponent), + multi: true + } + ], + standalone: false }) export class HereMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.ts index a5493468a0..338db7c24f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.ts @@ -38,21 +38,22 @@ import { EntityService } from '@core/http/entity.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-image-map-provider-settings', - templateUrl: './image-map-provider-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ImageMapProviderSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ImageMapProviderSettingsComponent), - multi: true - } - ] + selector: 'tb-image-map-provider-settings', + templateUrl: './image-map-provider-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ImageMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ImageMapProviderSettingsComponent), + multi: true + } + ], + standalone: false }) export class ImageMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-editor-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-editor-settings.component.ts index 398376f71a..9d127af2d8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-editor-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-editor-settings.component.ts @@ -33,21 +33,22 @@ import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-map-editor-settings', - templateUrl: './map-editor-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapEditorSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MapEditorSettingsComponent), - multi: true - } - ] + selector: 'tb-map-editor-settings', + templateUrl: './map-editor-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapEditorSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapEditorSettingsComponent), + multi: true + } + ], + standalone: false }) export class MapEditorSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.ts index 08b2a99e31..ea3b143db2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.ts @@ -47,21 +47,22 @@ import { IAliasController } from '@core/api/widget-api.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-map-provider-settings', - templateUrl: './map-provider-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapProviderSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MapProviderSettingsComponent), - multi: true - } - ] + selector: 'tb-map-provider-settings', + templateUrl: './map-provider-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapProviderSettingsComponent), + multi: true + } + ], + standalone: false }) export class MapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-settings-legacy.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-settings-legacy.component.ts index eb31c33075..5e98827469 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-settings-legacy.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-settings-legacy.component.ts @@ -53,21 +53,22 @@ import { Widget } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-map-settings-legacy', - templateUrl: './map-settings-legacy.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MapSettingsLegacyComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MapSettingsLegacyComponent), - multi: true - } - ] + selector: 'tb-map-settings-legacy', + templateUrl: './map-settings-legacy.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapSettingsLegacyComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapSettingsLegacyComponent), + multi: true + } + ], + standalone: false }) export class MapSettingsLegacyComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-widget-settings-legacy.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-widget-settings-legacy.component.ts index 3262e6b1cb..4a804c8e78 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-widget-settings-legacy.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-widget-settings-legacy.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { defaultMapSettings } from 'src/app/modules/home/components/widget/lib/maps-legacy/map-models'; @Component({ - selector: 'tb-map-widget-settings-legacy', - templateUrl: './map-widget-settings-legacy.component.html', - styleUrls: ['./../../widget-settings.scss'] + selector: 'tb-map-widget-settings-legacy', + templateUrl: './map-widget-settings-legacy.component.html', + styleUrls: ['./../../widget-settings.scss'], + standalone: false }) export class MapWidgetSettingsLegacyComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/marker-clustering-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/marker-clustering-settings.component.ts index c757d7805a..6615302230 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/marker-clustering-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/marker-clustering-settings.component.ts @@ -34,21 +34,22 @@ import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-marker-clustering-settings', - templateUrl: './marker-clustering-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MarkerClusteringSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MarkerClusteringSettingsComponent), - multi: true - } - ] + selector: 'tb-marker-clustering-settings', + templateUrl: './marker-clustering-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkerClusteringSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MarkerClusteringSettingsComponent), + multi: true + } + ], + standalone: false }) export class MarkerClusteringSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.ts index d890494f54..65421d0f53 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.ts @@ -36,21 +36,22 @@ import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-markers-settings', - templateUrl: './markers-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MarkersSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MarkersSettingsComponent), - multi: true - } - ] + selector: 'tb-markers-settings', + templateUrl: './markers-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkersSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MarkersSettingsComponent), + multi: true + } + ], + standalone: false }) export class MarkersSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.ts index ef650fc877..6a795edd84 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.ts @@ -37,21 +37,22 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-openstreet-map-provider-settings', - templateUrl: './openstreet-map-provider-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => OpenStreetMapProviderSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => OpenStreetMapProviderSettingsComponent), - multi: true - } - ] + selector: 'tb-openstreet-map-provider-settings', + templateUrl: './openstreet-map-provider-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => OpenStreetMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => OpenStreetMapProviderSettingsComponent), + multi: true + } + ], + standalone: false }) export class OpenStreetMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.ts index 129d268908..9b0b6e0110 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.ts @@ -39,21 +39,22 @@ import { Widget } from '@shared/models/widget.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-polygon-settings', - templateUrl: './polygon-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => PolygonSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => PolygonSettingsComponent), - multi: true - } - ] + selector: 'tb-polygon-settings', + templateUrl: './polygon-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PolygonSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PolygonSettingsComponent), + multi: true + } + ], + standalone: false }) export class PolygonSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/route-map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/route-map-settings.component.ts index 4a3cd9a3f8..cfba6811f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/route-map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/route-map-settings.component.ts @@ -33,21 +33,22 @@ import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-route-map-settings', - templateUrl: './route-map-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RouteMapSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => RouteMapSettingsComponent), - multi: true - } - ] + selector: 'tb-route-map-settings', + templateUrl: './route-map-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RouteMapSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RouteMapSettingsComponent), + multi: true + } + ], + standalone: false }) export class RouteMapSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/route-map-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/route-map-widget-settings.component.ts index 23bf193892..67e88aa7b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/route-map-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/route-map-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { defaultMapSettings } from 'src/app/modules/home/components/widget/lib/maps-legacy/map-models'; @Component({ - selector: 'tb-route-map-widget-settings', - templateUrl: './route-map-widget-settings.component.html', - styleUrls: ['./../../widget-settings.scss'] + selector: 'tb-route-map-widget-settings', + templateUrl: './route-map-widget-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + standalone: false }) export class RouteMapWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.ts index d3f81a7ff3..4e6e8284d1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.ts @@ -37,21 +37,22 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-tencent-map-provider-settings', - templateUrl: './tencent-map-provider-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TencentMapProviderSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TencentMapProviderSettingsComponent), - multi: true - } - ] + selector: 'tb-tencent-map-provider-settings', + templateUrl: './tencent-map-provider-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TencentMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TencentMapProviderSettingsComponent), + multi: true + } + ], + standalone: false }) export class TencentMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-common-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-common-settings.component.ts index 074cba4a5c..861188a6b9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-common-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-common-settings.component.ts @@ -35,21 +35,22 @@ import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-trip-animation-common-settings', - templateUrl: './trip-animation-common-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TripAnimationCommonSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TripAnimationCommonSettingsComponent), - multi: true - } - ] + selector: 'tb-trip-animation-common-settings', + templateUrl: './trip-animation-common-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripAnimationCommonSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripAnimationCommonSettingsComponent), + multi: true + } + ], + standalone: false }) export class TripAnimationCommonSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-marker-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-marker-settings.component.ts index f7ecd02f03..8f31d24f2f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-marker-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-marker-settings.component.ts @@ -34,21 +34,22 @@ import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-trip-animation-marker-settings', - templateUrl: './trip-animation-marker-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TripAnimationMarkerSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TripAnimationMarkerSettingsComponent), - multi: true - } - ] + selector: 'tb-trip-animation-marker-settings', + templateUrl: './trip-animation-marker-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripAnimationMarkerSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripAnimationMarkerSettingsComponent), + multi: true + } + ], + standalone: false }) export class TripAnimationMarkerSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.ts index 16c75cee56..9674ac2820 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.ts @@ -37,21 +37,22 @@ import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-trip-animation-path-settings', - templateUrl: './trip-animation-path-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TripAnimationPathSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TripAnimationPathSettingsComponent), - multi: true - } - ] + selector: 'tb-trip-animation-path-settings', + templateUrl: './trip-animation-path-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripAnimationPathSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripAnimationPathSettingsComponent), + multi: true + } + ], + standalone: false }) export class TripAnimationPathSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-point-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-point-settings.component.ts index 5e372051ad..8aae459e18 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-point-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-point-settings.component.ts @@ -38,21 +38,22 @@ import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-trip-animation-point-settings', - templateUrl: './trip-animation-point-settings.component.html', - styleUrls: ['./../../widget-settings.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TripAnimationPointSettingsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => TripAnimationPointSettingsComponent), - multi: true - } - ] + selector: 'tb-trip-animation-point-settings', + templateUrl: './trip-animation-point-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripAnimationPointSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripAnimationPointSettingsComponent), + multi: true + } + ], + standalone: false }) export class TripAnimationPointSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-widget-settings.component.ts index 440fd5df2c..5d4e28d7bb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-widget-settings.component.ts @@ -39,9 +39,10 @@ import { import { extractType } from '@core/utils'; @Component({ - selector: 'tb-trip-animation-widget-settings', - templateUrl: './trip-animation-widget-settings.component.html', - styleUrls: ['./../../widget-settings.scss'] + selector: 'tb-trip-animation-widget-settings', + templateUrl: './trip-animation-widget-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + standalone: false }) export class TripAnimationWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts index 296086b22f..3963f144fa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts @@ -24,9 +24,10 @@ import { mapWidgetDefaultSettings } from '@home/components/widget/lib/maps/map-w import { WidgetConfigComponentData } from '@home/models/widget-component.models'; @Component({ - selector: 'tb-map-widget-settings', - templateUrl: './map-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-map-widget-settings', + templateUrl: './map-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class MapWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.ts index 08ee515099..b3f6c2838f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-navigation-card-widget-settings', - templateUrl: './navigation-card-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-navigation-card-widget-settings', + templateUrl: './navigation-card-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class NavigationCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.ts index 3ffe4d6c22..fc17f555d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.ts @@ -26,9 +26,10 @@ import { Observable, of, Subject } from 'rxjs'; import { map, mergeMap, share, startWith } from 'rxjs/operators'; @Component({ - selector: 'tb-navigation-cards-widget-settings', - templateUrl: './navigation-cards-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-navigation-cards-widget-settings', + templateUrl: './navigation-cards-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class NavigationCardsWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.ts index 361a305421..cc04e67f2f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { scadaSymbolWidgetDefaultSettings } from '@home/components/widget/lib/scada/scada-symbol-widget.models'; @Component({ - selector: 'tb-scada-symbol-widget-settings', - templateUrl: './scada-symbol-widget-settings.component.html', - styleUrls: ['./../widget-settings.scss'] + selector: 'tb-scada-symbol-widget-settings', + templateUrl: './scada-symbol-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + standalone: false }) export class ScadaSymbolWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts index 96ea317109..102dc7ade6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts @@ -31,9 +31,10 @@ import { getDataKey } from '@shared/models/widget-settings.models'; import { getSourceTbUnitSymbol, TbUnit } from '@shared/models/unit.models'; @Component({ - selector: 'tb-wind-speed-direction-widget-settings', - templateUrl: './wind-speed-direction-widget-settings.component.html', - styleUrls: [] + selector: 'tb-wind-speed-direction-widget-settings', + templateUrl: './wind-speed-direction-widget-settings.component.html', + styleUrls: [], + standalone: false }) export class WindSpeedDirectionWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index a8414465a4..d363b6c56e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -158,9 +158,10 @@ interface TimeseriesTableSource { } @Component({ - selector: 'tb-timeseries-table-widget', - templateUrl: './timeseries-table-widget.component.html', - styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss'] + selector: 'tb-timeseries-table-widget', + templateUrl: './timeseries-table-widget.component.html', + styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss'], + standalone: false }) export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts index b69456ee30..9a7ea52d17 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts @@ -59,10 +59,11 @@ interface DataMap { } @Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'trip-animation', - templateUrl: './trip-animation.component.html', - styleUrls: ['./trip-animation.component.scss'] + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'trip-animation', + templateUrl: './trip-animation.component.html', + styleUrls: ['./trip-animation.component.scss'], + standalone: false }) export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts index 1c3570ad0f..dbc7bf889d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts @@ -68,10 +68,11 @@ const ticksTextMap: {[angle: number]: string} = { }; @Component({ - selector: 'tb-wind-speed-direction-widget', - templateUrl: './wind-speed-direction-widget.component.html', - styleUrls: ['./wind-speed-direction-widget.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-wind-speed-direction-widget', + templateUrl: './wind-speed-direction-widget.component.html', + styleUrls: ['./wind-speed-direction-widget.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WindSpeedDirectionWidgetComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 49652803a6..57b579d8e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -93,21 +93,22 @@ import { findWidgetModelDefinition } from '@shared/models/widget/widget-model.de import Timeout = NodeJS.Timeout; @Component({ - selector: 'tb-widget-config', - templateUrl: './widget-config.component.html', - styleUrls: ['./widget-config.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetConfigComponent), - multi: true - }, - { - provide: NG_ASYNC_VALIDATORS, - useExisting: forwardRef(() => WidgetConfigComponent), - multi: true, - } - ] + selector: 'tb-widget-config', + templateUrl: './widget-config.component.html', + styleUrls: ['./widget-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetConfigComponent), + multi: true + }, + { + provide: NG_ASYNC_VALIDATORS, + useExisting: forwardRef(() => WidgetConfigComponent), + multi: true, + } + ], + standalone: false }) export class WidgetConfigComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor, AsyncValidator { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index d8ff0cb1c1..3553ee3ae4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -67,11 +67,12 @@ export class WidgetComponentAction { // @dynamic @Component({ - selector: 'tb-widget-container', - templateUrl: './widget-container.component.html', - styleUrls: ['./widget-container.component.scss'], - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-widget-container', + templateUrl: './widget-container.component.html', + styleUrls: ['./widget-container.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class WidgetContainerComponent extends PageComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy { @@ -394,7 +395,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O } @Component({ - template: ` + template: `
{{ 'widget.reference' | translate }} @@ -431,8 +432,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
`, - styles: [], - encapsulation: ViewEncapsulation.None + styles: [], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class EditWidgetActionsTooltipComponent implements AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts index ea2770d0e5..d940696078 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts @@ -24,9 +24,10 @@ import { deepClone } from '@core/utils'; import { Timewindow } from '@shared/models/time/time.models'; @Component({ - selector: 'tb-widget-preview', - templateUrl: './widget-preview.component.html', - styleUrls: ['./widget-preview.component.scss'] + selector: 'tb-widget-preview', + templateUrl: './widget-preview.component.html', + styleUrls: ['./widget-preview.component.scss'], + standalone: false }) export class WidgetPreviewComponent extends PageComponent implements OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index d3ebd60b7c..f70d7a751b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -130,11 +130,12 @@ import { HttpClient } from '@angular/common/http'; import { addDiagnosticChain } from '@angular/compiler-cli/src/ngtsc/diagnostics'; @Component({ - selector: 'tb-widget', - templateUrl: './widget.component.html', - styleUrls: ['./widget.component.scss'], - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-widget', + templateUrl: './widget.component.html', + styleUrls: ['./widget.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class WidgetComponent extends PageComponent implements OnInit, OnChanges, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 6c991d492a..c88be6b550 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -35,9 +35,10 @@ import { CustomerId } from '@shared/models/id/customer-id'; import { HttpErrorResponse } from '@angular/common/http'; @Component({ - selector: 'tb-device-wizard', - templateUrl: './device-wizard-dialog.component.html', - styleUrls: ['./device-wizard-dialog.component.scss'] + selector: 'tb-device-wizard', + templateUrl: './device-wizard-dialog.component.html', + styleUrls: ['./device-wizard-dialog.component.scss'], + standalone: false }) export class DeviceWizardDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts index 31020d49a4..dd11ea16c8 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts @@ -36,10 +36,11 @@ export interface AddEntitiesToCustomerDialogData { } @Component({ - selector: 'tb-add-entities-to-customer-dialog', - templateUrl: './add-entities-to-customer-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AddEntitiesToCustomerDialogComponent}], - styleUrls: [] + selector: 'tb-add-entities-to-customer-dialog', + templateUrl: './add-entities-to-customer-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AddEntitiesToCustomerDialogComponent }], + styleUrls: [], + standalone: false }) export class AddEntitiesToCustomerDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts index 18abe0c30e..e2c4568c8e 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts @@ -38,10 +38,11 @@ export interface AddEntitiesToEdgeDialogData { } @Component({ - selector: 'tb-add-entities-to-edge-dialog', - templateUrl: './add-entities-to-edge-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AddEntitiesToEdgeDialogComponent}], - styleUrls: [] + selector: 'tb-add-entities-to-edge-dialog', + templateUrl: './add-entities-to-edge-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AddEntitiesToEdgeDialogComponent }], + styleUrls: [], + standalone: false }) export class AddEntitiesToEdgeDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts index 7bc7ad92a2..541312bf1a 100644 --- a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts @@ -36,10 +36,11 @@ export interface AssignToCustomerDialogData { } @Component({ - selector: 'tb-assign-to-customer-dialog', - templateUrl: './assign-to-customer-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AssignToCustomerDialogComponent}], - styleUrls: [] + selector: 'tb-assign-to-customer-dialog', + templateUrl: './assign-to-customer-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AssignToCustomerDialogComponent }], + styleUrls: [], + standalone: false }) export class AssignToCustomerDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { 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 e82a0b987c..3700be3cd5 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 @@ -37,9 +37,10 @@ export interface EventsDialogData { } @Component({ - selector: 'tb-debug-dialog', - templateUrl: './events-dialog.component.html', - styleUrl: './events-dialog.component.scss' + selector: 'tb-debug-dialog', + templateUrl: './events-dialog.component.html', + styleUrl: './events-dialog.component.scss', + standalone: false }) export class EventsDialogComponent extends DialogComponent implements AfterViewInit{ diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts index 25edc77b33..6191280933 100644 --- a/ui-ngx/src/app/modules/home/home.component.ts +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -36,9 +36,10 @@ import { ActivatedRoute } from '@angular/router'; import { isDefined, isDefinedAndNotNull } from '@core/utils'; @Component({ - selector: 'tb-home', - templateUrl: './home.component.html', - styleUrls: ['./home.component.scss'] + selector: 'tb-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'], + standalone: false }) export class HomeComponent extends PageComponent implements AfterViewInit, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts index 20b8957b15..95abc6623b 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -18,10 +18,11 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core import { MenuSection } from '@core/services/menu.models'; @Component({ - selector: 'tb-menu-link', - templateUrl: './menu-link.component.html', - styleUrls: ['./menu-link.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-menu-link', + templateUrl: './menu-link.component.html', + styleUrls: ['./menu-link.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class MenuLinkComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts index cd00627433..4415e9f580 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts @@ -22,10 +22,11 @@ import { AppState } from '@core/core.state'; import { ActionPreferencesUpdateOpenedMenuSection } from '@core/auth/auth.actions'; @Component({ - selector: 'tb-menu-toggle', - templateUrl: './menu-toggle.component.html', - styleUrls: ['./menu-toggle.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-menu-toggle', + templateUrl: './menu-toggle.component.html', + styleUrls: ['./menu-toggle.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class MenuToggleComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts index 6194146781..f266cee1a8 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts @@ -19,10 +19,11 @@ import { MenuService } from '@core/services/menu.service'; import { MenuSection } from '@core/services/menu.models'; @Component({ - selector: 'tb-side-menu', - templateUrl: './side-menu.component.html', - styleUrls: ['./side-menu.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-side-menu', + templateUrl: './side-menu.component.html', + styleUrls: ['./side-menu.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class SideMenuComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.ts index 950f1606c2..94134bb100 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.ts @@ -25,9 +25,10 @@ import { selectHasRepository } from '@core/auth/auth.selectors'; import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component'; @Component({ - selector: 'tb-auto-commit-admin-settings', - templateUrl: './auto-commit-admin-settings.component.html', - styleUrls: [] + selector: 'tb-auto-commit-admin-settings', + templateUrl: './auto-commit-admin-settings.component.html', + styleUrls: [], + standalone: false }) export class AutoCommitAdminSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts index e4634370d3..72543b5e6b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts @@ -31,9 +31,10 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-general-settings', - templateUrl: './general-settings.component.html', - styleUrls: ['./general-settings.component.scss', './settings-card.scss'] + selector: 'tb-general-settings', + templateUrl: './general-settings.component.html', + styleUrls: ['./general-settings.component.scss', './settings-card.scss'], + standalone: false }) export class GeneralSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.ts index a02d27f1bb..b518c4138d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.ts @@ -27,9 +27,10 @@ import { isDefinedAndNotNull } from '@core/utils'; import { DashboardId } from '@shared/models/id/dashboard-id'; @Component({ - selector: 'tb-home-settings', - templateUrl: './home-settings.component.html', - styleUrls: ['./home-settings.component.scss', './settings-card.scss'] + selector: 'tb-home-settings', + templateUrl: './home-settings.component.html', + styleUrls: ['./home-settings.component.scss', './settings-card.scss'], + standalone: false }) export class HomeSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts index c5126d5d10..bc226933b4 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts @@ -38,9 +38,10 @@ import { DomainSchema, domainSchemaTranslations, } from '@shared/models/oauth2.m import { WINDOW } from '@core/services/window.service'; @Component({ - selector: 'tb-mail-server', - templateUrl: './mail-server.component.html', - styleUrls: ['./mail-server.component.scss', './settings-card.scss'] + selector: 'tb-mail-server', + templateUrl: './mail-server.component.html', + styleUrls: ['./mail-server.component.scss', './settings-card.scss'], + standalone: false }) export class MailServerComponent extends PageComponent implements OnInit, OnDestroy, HasConfirmForm { adminSettings: AdminSettings; diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts index 26cbc1002d..ef2c44e8de 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts @@ -27,10 +27,11 @@ import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.compone import { ErrorStateMatcher } from '@angular/material/core'; @Component({ - selector: 'tb-client-dialog', - templateUrl: './client-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: ClientDialogComponent}], - styleUrls: [] + selector: 'tb-client-dialog', + templateUrl: './client-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: ClientDialogComponent }], + styleUrls: [], + standalone: false }) export class ClientDialogComponent extends DialogComponent implements OnDestroy, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts index 3ec2eed2f0..b0d28a8a1e 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts @@ -22,9 +22,10 @@ import { OAuth2Client, OAuth2ClientInfo } from '@shared/models/oauth2.models'; import { PageLink } from '@shared/models/page/page-link'; @Component({ - selector: 'tb-client-table-header', - templateUrl: './client-table-header.component.html', - styleUrls: [] + selector: 'tb-client-table-header', + templateUrl: './client-table-header.component.html', + styleUrls: [], + standalone: false }) export class ClientTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts index 7298f339f2..151b56df6c 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts @@ -44,9 +44,10 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; @Component({ - selector: 'tb-client', - templateUrl: './client.component.html', - styleUrls: ['./client.component.scss'] + selector: 'tb-client', + templateUrl: './client.component.html', + styleUrls: ['./client.component.scss'], + standalone: false }) export class ClientComponent extends EntityComponent implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts index 91e18ec8b3..ff94740791 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts @@ -21,9 +21,10 @@ import { AppState } from '@core/core.state'; import { DomainInfo } from '@shared/models/oauth2.models'; @Component({ - selector: 'tb-domain-table-header', - templateUrl: './domain-table-header.component.html', - styleUrls: [] + selector: 'tb-domain-table-header', + templateUrl: './domain-table-header.component.html', + styleUrls: [], + standalone: false }) export class DomainTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts index ffa4934480..f01c14b19f 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts @@ -31,9 +31,10 @@ import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-d import { EntityType } from '@shared/models/entity-type.models'; @Component({ - selector: 'tb-domain', - templateUrl: './domain.component.html', - styleUrls: [] + selector: 'tb-domain', + templateUrl: './domain.component.html', + styleUrls: [], + standalone: false }) export class DomainComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts index 371fab4a69..082243c6ac 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts @@ -26,9 +26,10 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod import { ActionNotificationShow } from '@core/notification/notification.actions'; @Component({ - selector: 'tb-queue', - templateUrl: './queue.component.html', - styleUrls: [] + selector: 'tb-queue', + templateUrl: './queue.component.html', + styleUrls: [], + standalone: false }) export class QueueComponent extends EntityComponent { entityForm: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/pages/admin/repository-admin-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/repository-admin-settings.component.ts index 46927f4671..4390484467 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/repository-admin-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/repository-admin-settings.component.ts @@ -23,9 +23,10 @@ import { UntypedFormGroup } from '@angular/forms'; import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component'; @Component({ - selector: 'tb-repository-admin-settings', - templateUrl: './repository-admin-settings.component.html', - styleUrls: [] + selector: 'tb-repository-admin-settings', + templateUrl: './repository-admin-settings.component.html', + styleUrls: [], + standalone: false }) export class RepositoryAdminSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts index 814ae7cf34..83d6248c92 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts @@ -22,9 +22,10 @@ import { Resource, ResourceInfo, ResourceSubType, ResourceSubTypeTranslationMap import { PageLink } from '@shared/models/page/page-link'; @Component({ - selector: 'tb-js-library-table-header', - templateUrl: './js-library-table-header.component.html', - styleUrls: [] + selector: 'tb-js-library-table-header', + templateUrl: './js-library-table-header.component.html', + styleUrls: [], + standalone: false }) export class JsLibraryTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts index c31d918337..0c87d02563 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts @@ -36,8 +36,9 @@ import { base64toString, isDefinedAndNotNull, stringToBase64 } from '@core/utils import { getCurrentAuthState } from '@core/auth/auth.selectors'; @Component({ - selector: 'tb-js-resource', - templateUrl: './js-resource.component.html' + selector: 'tb-js-resource', + templateUrl: './js-resource.component.html', + standalone: false }) export class JsResourceComponent extends EntityComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts index 22599d7d73..4ed898bcc2 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { NULL_UUID } from '@shared/models/id/has-uuid'; @Component({ - selector: 'tb-resource-library-tabs', - templateUrl: './resource-library-tabs.component.html', - styleUrls: [] + selector: 'tb-resource-library-tabs', + templateUrl: './resource-library-tabs.component.html', + styleUrls: [], + standalone: false }) export class ResourceLibraryTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.ts index bd831acdc6..ff12763bfc 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { NULL_UUID } from '@shared/models/id/has-uuid'; @Component({ - selector: 'tb-resource-tabs', - templateUrl: './resource-tabs.component.html', - styleUrls: [] + selector: 'tb-resource-tabs', + templateUrl: './resource-tabs.component.html', + styleUrls: [], + standalone: false }) export class ResourceTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts index 8b7ff4babc..33ed389a29 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts @@ -22,9 +22,10 @@ import { Resource, ResourceInfo, ResourceType, ResourceTypeTranslationMap } from import { PageLink } from '@shared/models/page/page-link'; @Component({ - selector: 'tb-resources-table-header', - templateUrl: './resources-table-header.component.html', - styleUrls: [] + selector: 'tb-resources-table-header', + templateUrl: './resources-table-header.component.html', + styleUrls: [], + standalone: false }) export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts index 74c28f05ad..12375a68ac 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -40,9 +40,10 @@ import { Observable, of } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-security-settings', - templateUrl: './security-settings.component.html', - styleUrls: ['./security-settings.component.scss', './settings-card.scss'] + selector: 'tb-security-settings', + templateUrl: './security-settings.component.html', + styleUrls: ['./security-settings.component.scss', './settings-card.scss'], + standalone: false }) export class SecuritySettingsComponent extends PageComponent implements HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.ts index 73687aabb3..f13dd74daa 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.ts @@ -31,9 +31,10 @@ export interface SendTestSmsDialogData { } @Component({ - selector: 'tb-send-test-sms-dialog', - templateUrl: './send-test-sms-dialog.component.html', - styleUrls: [] + selector: 'tb-send-test-sms-dialog', + templateUrl: './send-test-sms-dialog.component.html', + styleUrls: [], + standalone: false }) export class SendTestSmsDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts index fa8bca245e..ed259cecee 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts @@ -33,9 +33,10 @@ import { AuthUser } from '@shared/models/user.model'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @Component({ - selector: 'tb-sms-provider', - templateUrl: './sms-provider.component.html', - styleUrls: ['./sms-provider.component.scss', './settings-card.scss'] + selector: 'tb-sms-provider', + templateUrl: './sms-provider.component.html', + styleUrls: ['./sms-provider.component.scss', './settings-card.scss'], + standalone: false }) export class SmsProviderComponent extends PageComponent implements HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts index 8b9d73c73f..44b5dd3b6e 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts @@ -26,9 +26,10 @@ import { AppState } from "@core/core.state"; import { ActionAuthUpdateTrendzSettings } from "@core/auth/auth.actions"; @Component({ - selector: 'tb-trendz-settings', - templateUrl: './trendz-settings.component.html', - styleUrls: ['./trendz-settings.component.scss', './settings-card.scss'] + selector: 'tb-trendz-settings', + templateUrl: './trendz-settings.component.html', + styleUrls: ['./trendz-settings.component.scss', './settings-card.scss'], + standalone: false }) export class TrendzSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts index 6995d41879..f27976f07d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -35,9 +35,10 @@ import { EntityType } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-2fa-settings', - templateUrl: './two-factor-auth-settings.component.html', - styleUrls: [ './settings-card.scss', './two-factor-auth-settings.component.scss'] + selector: 'tb-2fa-settings', + templateUrl: './two-factor-auth-settings.component.html', + styleUrls: ['./settings-card.scss', './two-factor-auth-settings.component.scss'], + standalone: false }) export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts index f416e75033..fb9d1e814e 100644 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts @@ -21,14 +21,15 @@ import { AppState } from '@core/core.state'; import { AiModel } from '@shared/models/ai-model.models'; @Component({ - selector: 'tb-ai-model-table-header', - templateUrl: './ai-model-table-header.component.html', - styles: [` + selector: 'tb-ai-model-table-header', + templateUrl: './ai-model-table-header.component.html', + styles: [` :host { width: 100%; } `], - styleUrls: [] + styleUrls: [], + standalone: false }) export class AiModelTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts index 003c1f3b71..ae4ee4001d 100644 --- a/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts @@ -24,9 +24,10 @@ import type { import { debugCfActionEnabled } from '@shared/models/calculated-field.models'; @Component({ - selector: 'tb-alarm-rules-tabs', - templateUrl: './alarm-rules-tabs.component.html', - styleUrls: [] + selector: 'tb-alarm-rules-tabs', + templateUrl: './alarm-rules-tabs.component.html', + styleUrls: [], + standalone: false }) export class AlarmRulesTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.ts index 836e46d2fe..a7ce0082cd 100644 --- a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.ts @@ -21,9 +21,10 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone import { AssetProfile } from '@shared/models/asset.models'; @Component({ - selector: 'tb-asset-profile-tabs', - templateUrl: './asset-profile-tabs.component.html', - styleUrls: [] + selector: 'tb-asset-profile-tabs', + templateUrl: './asset-profile-tabs.component.html', + styleUrls: [], + standalone: false }) export class AssetProfileTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.ts index 8be127163e..d65362733e 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.ts @@ -23,9 +23,10 @@ import { AssetInfo } from '@shared/models/asset.models'; import { AssetProfileId } from '@shared/models/id/asset-profile-id'; @Component({ - selector: 'tb-asset-table-header', - templateUrl: './asset-table-header.component.html', - styleUrls: [] + selector: 'tb-asset-table-header', + templateUrl: './asset-table-header.component.html', + styleUrls: [], + standalone: false }) export class AssetTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts index dace342fab..a75f20dfca 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts @@ -22,9 +22,10 @@ import { AssetInfo } from '@app/shared/models/asset.models'; import { EntityId } from "@shared/models/id/entity-id"; @Component({ - selector: 'tb-asset-tabs', - templateUrl: './asset-tabs.component.html', - styleUrls: [] + selector: 'tb-asset-tabs', + templateUrl: './asset-tabs.component.html', + styleUrls: [], + standalone: false }) export class AssetTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts index bd6d94b1e0..4f8e3b7c63 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts @@ -27,9 +27,10 @@ import { AssetInfo } from '@app/shared/models/asset.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; @Component({ - selector: 'tb-asset', - templateUrl: './asset.component.html', - styleUrls: ['./asset.component.scss'] + selector: 'tb-asset', + templateUrl: './asset.component.html', + styleUrls: ['./asset.component.scss'], + standalone: false }) export class AssetComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts index acaacef7a7..8e441a5088 100644 --- a/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts @@ -24,9 +24,10 @@ import type { import { debugCfActionEnabled } from '@shared/models/calculated-field.models'; @Component({ - selector: 'tb-calculated-fields-tabs', - templateUrl: './calculated-fields-tabs.component.html', - styleUrls: [] + selector: 'tb-calculated-fields-tabs', + templateUrl: './calculated-fields-tabs.component.html', + styleUrls: [], + standalone: false }) export class CalculatedFieldsTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.ts index 13afac05ac..0bf2f691a0 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.ts @@ -21,9 +21,10 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone import { Customer } from '@shared/models/customer.model'; @Component({ - selector: 'tb-customer-tabs', - templateUrl: './customer-tabs.component.html', - styleUrls: [] + selector: 'tb-customer-tabs', + templateUrl: './customer-tabs.component.html', + styleUrls: [], + standalone: false }) export class CustomerTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts index 6a90ac0b03..0eb5681cc2 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts @@ -29,9 +29,10 @@ import { AuthState } from '@core/auth/auth.models'; import { CountryData } from '@shared/models/country.models'; @Component({ - selector: 'tb-customer', - templateUrl: './customer.component.html', - styleUrls: ['./customer.component.scss'] + selector: 'tb-customer', + templateUrl: './customer.component.html', + styleUrls: ['./customer.component.scss'], + standalone: false }) export class CustomerComponent extends ContactBasedComponent { diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index a404496266..13e1d2664d 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -34,9 +34,10 @@ import { EntityType } from '@shared/models/entity-type.models'; import { PageLink } from "@shared/models/page/page-link"; @Component({ - selector: 'tb-dashboard-form', - templateUrl: './dashboard-form.component.html', - styleUrls: ['./dashboard-form.component.scss'] + selector: 'tb-dashboard-form', + templateUrl: './dashboard-form.component.html', + styleUrls: ['./dashboard-form.component.scss'], + standalone: false }) export class DashboardFormComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.ts index 88610d212c..52e4c180a8 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.ts @@ -21,9 +21,10 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone import { Dashboard } from '@shared/models/dashboard.models'; @Component({ - selector: 'tb-dashboard-tabs', - templateUrl: './dashboard-tabs.component.html', - styleUrls: [] + selector: 'tb-dashboard-tabs', + templateUrl: './dashboard-tabs.component.html', + styleUrls: [], + standalone: false }) export class DashboardTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts index d5ea2a9849..336043907f 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts @@ -30,9 +30,10 @@ export interface DashboardInfoDialogData { } @Component({ - selector: 'tb-import-dashboard-file-dialog', - templateUrl: './import-dashboard-file-dialog.component.html', - styleUrls: [] + selector: 'tb-import-dashboard-file-dialog', + templateUrl: './import-dashboard-file-dialog.component.html', + styleUrls: [], + standalone: false }) export class ImportDashboardFileDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.ts index 0af06fd787..ede15ab957 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.ts @@ -31,9 +31,10 @@ export interface MakeDashboardPublicDialogData { } @Component({ - selector: 'tb-make-dashboard-public-dialog', - templateUrl: './make-dashboard-public-dialog.component.html', - styleUrls: [] + selector: 'tb-make-dashboard-public-dialog', + templateUrl: './make-dashboard-public-dialog.component.html', + styleUrls: [], + standalone: false }) export class MakeDashboardPublicDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.ts index eb25e7cba1..ab1da02eef 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.ts @@ -35,10 +35,11 @@ export interface ManageDashboardCustomersDialogData { } @Component({ - selector: 'tb-manage-dashboard-customers-dialog', - templateUrl: './manage-dashboard-customers-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardCustomersDialogComponent}], - styleUrls: [] + selector: 'tb-manage-dashboard-customers-dialog', + templateUrl: './manage-dashboard-customers-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: ManageDashboardCustomersDialogComponent }], + styleUrls: [], + standalone: false }) export class ManageDashboardCustomersDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts index 1aadb36778..0560f88174 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts @@ -27,9 +27,10 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device-profile-tabs', - templateUrl: './device-profile-tabs.component.html', - styleUrls: [] + selector: 'tb-device-profile-tabs', + templateUrl: './device-profile-tabs.component.html', + styleUrls: [], + standalone: false }) export class DeviceProfileTabsComponent extends EntityTabsComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/coap-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/coap-device-transport-configuration.component.ts index 0dce317071..15c5a7eaa6 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/coap-device-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/coap-device-transport-configuration.component.ts @@ -29,14 +29,15 @@ import { takeUntil } from 'rxjs/operators'; import { isDefinedAndNotNull } from '@core/utils'; @Component({ - selector: 'tb-coap-device-transport-configuration', - templateUrl: './coap-device-transport-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CoapDeviceTransportConfigurationComponent), - multi: true - }] + selector: 'tb-coap-device-transport-configuration', + templateUrl: './coap-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CoapDeviceTransportConfigurationComponent), + multi: true + }], + standalone: false }) export class CoapDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts index 4e4c4b51a1..ff39e03bb0 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts @@ -27,14 +27,15 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-default-device-configuration', - templateUrl: './default-device-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DefaultDeviceConfigurationComponent), - multi: true - }] + selector: 'tb-default-device-configuration', + templateUrl: './default-device-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceConfigurationComponent), + multi: true + }], + standalone: false }) export class DefaultDeviceConfigurationComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts index 1e196210fc..fe6238ab90 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts @@ -27,14 +27,15 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-default-device-transport-configuration', - templateUrl: './default-device-transport-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DefaultDeviceTransportConfigurationComponent), - multi: true - }] + selector: 'tb-default-device-transport-configuration', + templateUrl: './default-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceTransportConfigurationComponent), + multi: true + }], + standalone: false }) export class DefaultDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts index 54432ccf11..aab6c76339 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts @@ -24,14 +24,15 @@ import { deepClone } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device-configuration', - templateUrl: './device-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceConfigurationComponent), - multi: true - }] + selector: 'tb-device-configuration', + templateUrl: './device-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceConfigurationComponent), + multi: true + }], + standalone: false }) export class DeviceConfigurationComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts index 9acf174e4b..c6f03a84f1 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts @@ -36,21 +36,22 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device-data', - templateUrl: './device-data.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceDataComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceDataComponent), - multi: true - }, - ] + selector: 'tb-device-data', + templateUrl: './device-data.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceDataComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceDataComponent), + multi: true + }, + ], + standalone: false }) export class DeviceDataComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts index ccc7f20c5e..96456ebe14 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts @@ -33,20 +33,22 @@ import { deepClone } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device-transport-configuration', - templateUrl: './device-transport-configuration.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DeviceTransportConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => DeviceTransportConfigurationComponent), - multi: true - }] + selector: 'tb-device-transport-configuration', + templateUrl: './device-transport-configuration.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceTransportConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceTransportConfigurationComponent), + multi: true + } + ], + standalone: false }) export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts index 930b62b9f8..aa11e891d8 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts @@ -29,14 +29,15 @@ import { Subject } from 'rxjs'; import { isDefinedAndNotNull } from '@core/utils'; @Component({ - selector: 'tb-lwm2m-device-transport-configuration', - templateUrl: './lwm2m-device-transport-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => Lwm2mDeviceTransportConfigurationComponent), - multi: true - }] + selector: 'tb-lwm2m-device-transport-configuration', + templateUrl: './lwm2m-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mDeviceTransportConfigurationComponent), + multi: true + }], + standalone: false }) export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts index f409713ed3..414fe213bc 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts @@ -26,14 +26,15 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-mqtt-device-transport-configuration', - templateUrl: './mqtt-device-transport-configuration.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MqttDeviceTransportConfigurationComponent), - multi: true - }] + selector: 'tb-mqtt-device-transport-configuration', + templateUrl: './mqtt-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MqttDeviceTransportConfigurationComponent), + multi: true + }], + standalone: false }) export class MqttDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts index 1c87b0d95d..c27b4eed9f 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts @@ -42,19 +42,21 @@ import { isDefinedAndNotNull } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-snmp-device-transport-configuration', - templateUrl: './snmp-device-transport-configuration.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent), - multi: true - }, { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent), - multi: true - }] + selector: 'tb-snmp-device-transport-configuration', + templateUrl: './snmp-device-transport-configuration.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent), + multi: true + } + ], + standalone: false }) export class SnmpDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts index 77bd019133..2385d1a933 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts @@ -50,9 +50,10 @@ export interface DeviceCheckConnectivityDialogData { } @Component({ - selector: 'tb-device-check-connectivity-dialog', - templateUrl: './device-check-connectivity-dialog.component.html', - styleUrls: ['./device-check-connectivity-dialog.component.scss'] + selector: 'tb-device-check-connectivity-dialog', + templateUrl: './device-check-connectivity-dialog.component.html', + styleUrls: ['./device-check-connectivity-dialog.component.scss'], + standalone: false }) export class DeviceCheckConnectivityDialogComponent extends DialogComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts index 470604499c..4eda9c96d8 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts @@ -36,10 +36,11 @@ export interface DeviceCredentialsDialogData { } @Component({ - selector: 'tb-device-credentials-dialog', - templateUrl: './device-credentials-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent}], - styleUrls: ['./device-credentials-dialog.component.scss'] + selector: 'tb-device-credentials-dialog', + templateUrl: './device-credentials-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent }], + styleUrls: ['./device-credentials-dialog.component.scss'], + standalone: false }) export class DeviceCredentialsDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts index a51d21f9b2..d65f21d312 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts @@ -23,9 +23,10 @@ import { EntityType } from '@shared/models/entity-type.models'; import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id'; @Component({ - selector: 'tb-device-table-header', - templateUrl: './device-table-header.component.html', - styleUrls: [] + selector: 'tb-device-table-header', + templateUrl: './device-table-header.component.html', + styleUrls: [], + standalone: false }) export class DeviceTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts index 01e186612b..96c23db9d2 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts @@ -22,9 +22,10 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone import { EntityId } from "@shared/models/id/entity-id"; @Component({ - selector: 'tb-device-tabs', - templateUrl: './device-tabs.component.html', - styleUrls: [] + selector: 'tb-device-tabs', + templateUrl: './device-tabs.component.html', + styleUrls: [], + standalone: false }) export class DeviceTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index c5e798c0ff..de4ec0ae27 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -39,9 +39,10 @@ import { distinctUntilChanged } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-device', - templateUrl: './device.component.html', - styleUrls: ['./device.component.scss'] + selector: 'tb-device', + templateUrl: './device.component.html', + styleUrls: ['./device.component.scss'], + standalone: false }) export class DeviceComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts index 41ed81b1f4..f907f11238 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts @@ -39,9 +39,10 @@ export interface EdgeInstructionsDialogData { } @Component({ - selector: 'tb-edge-installation-dialog', - templateUrl: './edge-instructions-dialog.component.html', - styleUrls: ['./edge-instructions-dialog.component.scss'] + selector: 'tb-edge-installation-dialog', + templateUrl: './edge-instructions-dialog.component.html', + styleUrls: ['./edge-instructions-dialog.component.scss'], + standalone: false }) export class EdgeInstructionsDialogComponent extends DialogComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-table-header.component.ts index 054b6626b4..c1111fa449 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-table-header.component.ts @@ -22,9 +22,10 @@ import { AppState } from '@core/core.state'; import { EdgeInfo } from '@shared/models/edge.models'; @Component({ - selector: 'tb-edge-table-header', - templateUrl: './edge-table-header.component.html', - styleUrls: [] + selector: 'tb-edge-table-header', + templateUrl: './edge-table-header.component.html', + styleUrls: [], + standalone: false }) export class EdgeTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.ts index 48a19840c2..58bc88a8db 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.ts @@ -21,9 +21,10 @@ import { EdgeInfo } from '@shared/models/edge.models'; import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; @Component({ - selector: 'tb-edge-tabs', - templateUrl: './edge-tabs.component.html', - styleUrls: [] + selector: 'tb-edge-tabs', + templateUrl: './edge-tabs.component.html', + styleUrls: [], + standalone: false }) export class EdgeTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts index 396ba8b9b5..7a0d82798c 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts @@ -29,9 +29,10 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod import {EdgeService} from "@core/http/edge.service"; @Component({ - selector: 'tb-edge', - templateUrl: './edge.component.html', - styleUrls: ['./edge.component.scss'] + selector: 'tb-edge', + templateUrl: './edge.component.html', + styleUrls: ['./edge.component.scss'], + standalone: false }) export class EdgeComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.ts index 782d1d2c83..da2a867c56 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-table-header.component.ts @@ -22,9 +22,10 @@ import { EntityType } from '@shared/models/entity-type.models'; import { EntityViewInfo } from '@app/shared/models/entity-view.models'; @Component({ - selector: 'tb-entity-view-table-header', - templateUrl: './entity-view-table-header.component.html', - styleUrls: [] + selector: 'tb-entity-view-table-header', + templateUrl: './entity-view-table-header.component.html', + styleUrls: [], + standalone: false }) export class EntityViewTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.ts index 26d678c45b..577b4eb147 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.ts @@ -21,9 +21,10 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone import { EntityViewInfo } from '@app/shared/models/entity-view.models'; @Component({ - selector: 'tb-entity-view-tabs', - templateUrl: './entity-view-tabs.component.html', - styleUrls: [] + selector: 'tb-entity-view-tabs', + templateUrl: './entity-view-tabs.component.html', + styleUrls: [], + standalone: false }) export class EntityViewTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.ts index 98e1a55695..65485f340c 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.ts +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.ts @@ -30,9 +30,10 @@ import { EntityId } from '@app/shared/models/id/entity-id'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; @Component({ - selector: 'tb-entity-view', - templateUrl: './entity-view.component.html', - styleUrls: ['./entity-view.component.scss'] + selector: 'tb-entity-view', + templateUrl: './entity-view.component.html', + styleUrls: ['./entity-view.component.scss'], + standalone: false }) export class EntityViewComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts index 67546086cb..f7b38179ab 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts @@ -23,10 +23,11 @@ import { ActivatedRoute } from '@angular/router'; import { HomeDashboard } from '@shared/models/dashboard.models'; @Component({ - selector: 'tb-home-links', - templateUrl: './home-links.component.html', - styleUrls: ['./home-links.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-home-links', + templateUrl: './home-links.component.html', + styleUrls: ['./home-links.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class HomeLinksComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts index 1ea70bc9f6..7a9f72b7aa 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts @@ -33,10 +33,11 @@ export interface MobileAppDialogData { } @Component({ - selector: 'tb-mobile-app-dialog', - templateUrl: './mobile-app-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: MobileAppDialogComponent}], - styleUrls: [] + selector: 'tb-mobile-app-dialog', + templateUrl: './mobile-app-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: MobileAppDialogComponent }], + styleUrls: [], + standalone: false }) export class MobileAppDialogComponent extends DialogComponent implements OnDestroy, AfterViewInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts index 0711aa1116..d0b5f2f416 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-header.component.ts @@ -21,9 +21,10 @@ import { AppState } from '@core/core.state'; import { MobileApp } from '@shared/models/mobile-app.models'; @Component({ - selector: 'tb-mobile-app-table-header', - templateUrl: './mobile-app-table-header.component.html', - styleUrls: ['./mobile-app-table-header.component.scss'] + selector: 'tb-mobile-app-table-header', + templateUrl: './mobile-app-table-header.component.html', + styleUrls: ['./mobile-app-table-header.component.scss'], + standalone: false }) export class MobileAppTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts index abcb7beb5d..d2c1fe3dc9 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts @@ -31,9 +31,10 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { EditorPanelComponent } from '@home/pages/mobile/common/editor-panel.component'; @Component({ - selector: 'tb-mobile-app', - templateUrl: './mobile-app.component.html', - styleUrls: ['./mobile-app.component.scss'] + selector: 'tb-mobile-app', + templateUrl: './mobile-app.component.html', + styleUrls: ['./mobile-app.component.scss'], + standalone: false }) export class MobileAppComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.ts index 39d0c5e266..1843192844 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.ts @@ -32,9 +32,10 @@ export interface MobileAppDeleteDialogData { } @Component({ - selector: 'tb-remove-app-dialog', - templateUrl: './remove-app-dialog.component.html', - styleUrls: ['./remove-app-dialog.component.scss'] + selector: 'tb-remove-app-dialog', + templateUrl: './remove-app-dialog.component.html', + styleUrls: ['./remove-app-dialog.component.scss'], + standalone: false }) export class RemoveAppDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.ts index e3644c117f..bff9631ce4 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/add-mobile-page-dialog.component.ts @@ -25,9 +25,10 @@ import { FormBuilder } from '@angular/forms'; import { deepTrim } from '@core/utils'; @Component({ - selector: 'tb-add-mobile-page-dialog', - templateUrl: './add-mobile-page-dialog.component.html', - styleUrls: ['./add-mobile-page-dialog.component.scss'] + selector: 'tb-add-mobile-page-dialog', + templateUrl: './add-mobile-page-dialog.component.html', + styleUrls: ['./add-mobile-page-dialog.component.scss'], + standalone: false }) export class AddMobilePageDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.ts index 045f1e3298..afbdd00885 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page-panel.component.ts @@ -20,10 +20,11 @@ import { CustomMobilePage } from '@shared/models/mobile-app.models'; import { TbPopoverComponent } from '@shared/components/popover.component'; @Component({ - selector: 'tb-custom-menu-item-panel', - templateUrl: './custom-mobile-page-panel.component.html', - styleUrls: ['./custom-mobile-page-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-custom-menu-item-panel', + templateUrl: './custom-mobile-page-panel.component.html', + styleUrls: ['./custom-mobile-page-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class CustomMobilePagePanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts index 4c97990028..84652c3027 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts @@ -37,21 +37,22 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-mobile-page-item', - templateUrl: './custom-mobile-page.component.html', - styleUrls: ['./custom-mobile-page.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => CustomMobilePageComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => CustomMobilePageComponent), - multi: true - } - ] + selector: 'tb-mobile-page-item', + templateUrl: './custom-mobile-page.component.html', + styleUrls: ['./custom-mobile-page.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CustomMobilePageComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CustomMobilePageComponent), + multi: true + } + ], + standalone: false }) export class CustomMobilePageComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts index de3d7a1a79..e4c1d3b288 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.ts @@ -21,10 +21,11 @@ import { FormBuilder, Validators } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-default-mobile-page-panel', - templateUrl: './default-mobile-page-panel.component.html', - styleUrls: ['./default-mobile-page-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-default-mobile-page-panel', + templateUrl: './default-mobile-page-panel.component.html', + styleUrls: ['./default-mobile-page-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class DefaultMobilePagePanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts index b61263c2d5..9506f68043 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts @@ -44,21 +44,22 @@ import { MatDialog } from '@angular/material/dialog'; import { AddMobilePageDialogComponent } from '@home/pages/mobile/bundes/layout/add-mobile-page-dialog.component'; @Component({ - selector: 'tb-mobile-layout', - templateUrl: './mobile-layout.component.html', - styleUrls: ['./mobile-layout.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MobileLayoutComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MobileLayoutComponent), - multi: true - } - ], + selector: 'tb-mobile-layout', + templateUrl: './mobile-layout.component.html', + styleUrls: ['./mobile-layout.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MobileLayoutComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MobileLayoutComponent), + multi: true + } + ], + standalone: false }) export class MobileLayoutComponent implements ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts index 516e007761..8772b6ad71 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts @@ -58,22 +58,23 @@ import { TranslateService } from '@ngx-translate/core'; import { DisplayPopoverConfig } from '@shared/components/popover.models'; @Component({ - selector: 'tb-mobile-menu-item-row', - templateUrl: './mobile-page-item-row.component.html', - styleUrls: ['./mobile-page-item-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MobilePageItemRowComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MobilePageItemRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-mobile-menu-item-row', + templateUrl: './mobile-page-item-row.component.html', + styleUrls: ['./mobile-page-item-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MobilePageItemRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MobilePageItemRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts index 1f120de868..b03faa5b5a 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts @@ -33,9 +33,10 @@ export interface MobileAppConfigurationDialogData { } @Component({ - selector: 'tb-mobile-app-configuration-dialog', - templateUrl: './mobile-app-configuration-dialog.component.html', - styleUrls: ['./mobile-app-configuration-dialog.component.scss'] + selector: 'tb-mobile-app-configuration-dialog', + templateUrl: './mobile-app-configuration-dialog.component.html', + styleUrls: ['./mobile-app-configuration-dialog.component.scss'], + standalone: false }) export class MobileAppConfigurationDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts index 1f16673ef5..29b0999b80 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts @@ -45,9 +45,10 @@ export interface MobileBundleDialogData { } @Component({ - selector: 'tb-mobile-bundle-dialog', - templateUrl: './mobile-bundle-dialog.component.html', - styleUrls: ['./mobile-bundle-dialog.component.scss'] + selector: 'tb-mobile-bundle-dialog', + templateUrl: './mobile-bundle-dialog.component.html', + styleUrls: ['./mobile-bundle-dialog.component.scss'], + standalone: false }) export class MobileBundleDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.ts index 1a0a6aeca9..dc9c6492f8 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-header.component.ts @@ -21,9 +21,10 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @Component({ - selector: 'tb-mobile-bundle-table-header', - templateUrl: './mobile-bundle-table-header.component.html', - styleUrls: ['./mobile-bundle-table-header.component.scss'] + selector: 'tb-mobile-bundle-table-header', + templateUrl: './mobile-bundle-table-header.component.html', + styleUrls: ['./mobile-bundle-table-header.component.scss'], + standalone: false }) export class MobileBundleTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts index 49cad3a3ab..9f2e850b53 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts @@ -20,10 +20,11 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { EditorOptions } from 'tinymce'; @Component({ - selector: 'tb-release-notes-panel', - templateUrl: './editor-panel.component.html', - styleUrls: ['./editor-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-release-notes-panel', + templateUrl: './editor-panel.component.html', + styleUrls: ['./editor-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class EditorPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts index 9c6b58a967..1745a8a0b1 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.ts @@ -27,9 +27,10 @@ import { EntityType } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-mobile-qr-code-widget', - templateUrl: './mobile-qr-code-widget-settings.component.html', - styleUrls: ['mobile-qr-code-widget-settings.component.scss', '../../admin/settings-card.scss'] + selector: 'tb-mobile-qr-code-widget', + templateUrl: './mobile-qr-code-widget-settings.component.html', + styleUrls: ['mobile-qr-code-widget-settings.component.scss', '../../admin/settings-card.scss'], + standalone: false }) export class MobileQrCodeWidgetSettingsComponent extends PageComponent implements HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.ts index f1ce8808ef..2ab4382a2c 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-notification-dialog.component.ts @@ -27,9 +27,10 @@ export interface InboxNotificationDialogData { } @Component({ - selector: 'tb-inbox-notification-dialog', - templateUrl: './inbox-notification-dialog.component.html', - styleUrls: ['inbox-notification-dialog.component.scss'] + selector: 'tb-inbox-notification-dialog', + templateUrl: './inbox-notification-dialog.component.html', + styleUrls: ['inbox-notification-dialog.component.scss'], + standalone: false }) export class InboxNotificationDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.ts index f1b9bea7c7..7aade4fbad 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-header.component.ts @@ -21,9 +21,10 @@ import { AppState } from '@core/core.state'; import { Notification } from '@shared/models/notification.models'; @Component({ - selector: 'tb-inbox-table-header', - templateUrl: './inbox-table-header.component.html', - styleUrls: [] + selector: 'tb-inbox-table-header', + templateUrl: './inbox-table-header.component.html', + styleUrls: [], + standalone: false }) export class InboxTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts index 755d9d181d..deb518e93b 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts @@ -46,9 +46,10 @@ export interface RecipientNotificationDialogData { } @Component({ - selector: 'tb-target-notification-dialog', - templateUrl: './recipient-notification-dialog.component.html', - styleUrls: ['recipient-notification-dialog.component.scss'] + selector: 'tb-target-notification-dialog', + templateUrl: './recipient-notification-dialog.component.html', + styleUrls: ['recipient-notification-dialog.component.scss'], + standalone: false }) export class RecipientNotificationDialogComponent extends DialogComponent implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.ts index 3794d7ab26..faa6a565e3 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.ts @@ -42,21 +42,22 @@ import { MatDialog } from '@angular/material/dialog'; import { MatButton } from '@angular/material/button'; @Component({ - selector: 'tb-escalation-form', - templateUrl: './escalation-form.component.html', - styleUrls: ['./escalation-form.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EscalationFormComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => EscalationFormComponent), - multi: true, - } - ] + selector: 'tb-escalation-form', + templateUrl: './escalation-form.component.html', + styleUrls: ['./escalation-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EscalationFormComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EscalationFormComponent), + multi: true, + } + ], + standalone: false }) export class EscalationFormComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.ts index 36d2913621..37aa7393f8 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.ts @@ -35,21 +35,22 @@ import { takeUntil } from 'rxjs/operators'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-escalations-component', - templateUrl: './escalations.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => EscalationsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => EscalationsComponent), - multi: true, - } - ] + selector: 'tb-escalations-component', + templateUrl: './escalations.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EscalationsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EscalationsComponent), + multi: true, + } + ], + standalone: false }) export class EscalationsComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts index 629407ace3..5bfa64c99d 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts @@ -77,9 +77,10 @@ export interface RuleNotificationDialogData { } @Component({ - selector: 'tb-rule-notification-dialog', - templateUrl: './rule-notification-dialog.component.html', - styleUrls: ['rule-notification-dialog.component.scss'] + selector: 'tb-rule-notification-dialog', + templateUrl: './rule-notification-dialog.component.html', + styleUrls: ['rule-notification-dialog.component.scss'], + standalone: false }) export class RuleNotificationDialogComponent extends DialogComponent implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts index c20b49bc16..ea6e0bed5c 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts @@ -31,9 +31,10 @@ export interface NotificationRequestErrorDialogData { } @Component({ - selector: 'tb-notification-send-error-dialog', - templateUrl: './sent-error-dialog.component.html', - styleUrls: ['sent-error-dialog.component.scss'] + selector: 'tb-notification-send-error-dialog', + templateUrl: './sent-error-dialog.component.html', + styleUrls: ['sent-error-dialog.component.scss'], + standalone: false }) export class SentErrorDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts index 70a972066f..eec70df210 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts @@ -54,9 +54,10 @@ export interface RequestNotificationDialogData { } @Component({ - selector: 'tb-sent-notification-dialog', - templateUrl: './sent-notification-dialog.component.html', - styleUrls: ['./sent-notification-dialog.component.scss'] + selector: 'tb-sent-notification-dialog', + templateUrl: './sent-notification-dialog.component.html', + styleUrls: ['./sent-notification-dialog.component.scss'], + standalone: false }) export class SentNotificationDialogComponent extends TemplateConfiguration implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts index 187ae39d3d..071ccbeeb5 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts @@ -25,16 +25,17 @@ import { } from '@shared/models/notification.models'; @Component({ - selector: 'tb-notification-setting-form', - templateUrl: './notification-setting-form.component.html', - styleUrls: ['./notification-setting-form.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => NotificationSettingFormComponent), - multi: true - } - ] + selector: 'tb-notification-setting-form', + templateUrl: './notification-setting-form.component.html', + styleUrls: ['./notification-setting-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NotificationSettingFormComponent), + multi: true + } + ], + standalone: false }) export class NotificationSettingFormComponent implements ControlValueAccessor, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts index 3abddf49af..4adc4d4c6c 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts @@ -32,9 +32,10 @@ import { NotificationService } from '@core/http/notification.service'; import { DialogService } from '@core/services/dialog.service'; @Component({ - selector: 'tb-notification-settings', - templateUrl: './notification-settings.component.html', - styleUrls: ['./notification-settings.component.scss'] + selector: 'tb-notification-settings', + templateUrl: './notification-settings.component.html', + styleUrls: ['./notification-settings.component.scss'], + standalone: false }) export class NotificationSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts index cf828ce68f..5b3975a1a0 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts @@ -32,20 +32,21 @@ import { isDefinedAndNotNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-notification-action-button-configuration', - templateUrl: './notification-action-button-configuration.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => NotificationActionButtonConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => NotificationActionButtonConfigurationComponent), - multi: true, - } - ] + selector: 'tb-notification-action-button-configuration', + templateUrl: './notification-action-button-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NotificationActionButtonConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NotificationActionButtonConfigurationComponent), + multi: true, + } + ], + standalone: false }) export class NotificationActionButtonConfigurationComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts index d3a606b8b9..4a4f11d40b 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts @@ -40,21 +40,22 @@ import { TranslateService } from '@ngx-translate/core'; import { EditorOptions } from 'tinymce'; @Component({ - selector: 'tb-template-configuration', - templateUrl: './notification-template-configuration.component.html', - styleUrls: ['./notification-template-configuration.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => NotificationTemplateConfigurationComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => NotificationTemplateConfigurationComponent), - multi: true, - } - ] + selector: 'tb-template-configuration', + templateUrl: './notification-template-configuration.component.html', + styleUrls: ['./notification-template-configuration.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NotificationTemplateConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NotificationTemplateConfigurationComponent), + multi: true, + } + ], + standalone: false }) export class NotificationTemplateConfigurationComponent implements OnDestroy, ControlValueAccessor, Validator { 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 96aa8ba97a..47ea14c84d 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 @@ -44,9 +44,10 @@ export interface TemplateNotificationDialogData { } @Component({ - selector: 'tb-template-notification-dialog', - templateUrl: './template-notification-dialog.component.html', - styleUrls: ['./template-notification-dialog.component.scss'] + selector: 'tb-template-notification-dialog', + templateUrl: './template-notification-dialog.component.html', + styleUrls: ['./template-notification-dialog.component.scss'], + standalone: false }) export class TemplateNotificationDialogComponent extends TemplateConfiguration implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts index faa54215f7..31b2813d78 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts @@ -23,9 +23,10 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { OtaPackage } from '@shared/models/ota-package.models'; @Component({ - selector: 'tb-ota-update-tabs', - templateUrl: './ota-update-tabs.component.html', - styleUrls: [] + selector: 'tb-ota-update-tabs', + templateUrl: './ota-update-tabs.component.html', + styleUrls: [], + standalone: false }) export class OtaUpdateTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts index 5ec5ca42e7..8810d46627 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts @@ -34,8 +34,9 @@ import { filter, startWith, takeUntil } from 'rxjs/operators'; import { isNotEmptyStr } from '@core/utils'; @Component({ - selector: 'tb-ota-update', - templateUrl: './ota-update.component.html' + selector: 'tb-ota-update', + templateUrl: './ota-update.component.html', + standalone: false }) export class OtaUpdateComponent extends EntityComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts index 3ab555a9e2..04b04d0f7a 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts @@ -34,9 +34,10 @@ import { UnitSystem, UnitSystems } from '@shared/models/unit.models'; import { UnitService } from '@core/services/unit.service'; @Component({ - selector: 'tb-profile', - templateUrl: './profile.component.html', - styleUrls: ['./profile.component.scss'] + selector: 'tb-profile', + templateUrl: './profile.component.html', + styleUrls: ['./profile.component.scss'], + standalone: false }) export class ProfileComponent extends PageComponent implements OnInit, HasConfirmForm { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts index 0aa2d86e6f..6745796cbb 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts @@ -29,14 +29,15 @@ import { catchError, map, mergeMap, share, startWith } from 'rxjs/operators'; import { RuleChainService } from '@core/http/rule-chain.service'; @Component({ - selector: 'tb-link-labels', - templateUrl: './link-labels.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => LinkLabelsComponent), - multi: true - }] + selector: 'tb-link-labels', + templateUrl: './link-labels.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LinkLabelsComponent), + multi: true + }], + standalone: false }) export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts index e1d8f11bf5..013e4841f3 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts @@ -47,15 +47,16 @@ import { deepClone } from '@core/utils'; import { RuleChainType } from '@shared/models/rule-chain.models'; @Component({ - selector: 'tb-rule-node-config', - templateUrl: './rule-node-config.component.html', - styleUrls: ['./rule-node-config.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RuleNodeConfigComponent), - multi: true - }], - encapsulation: ViewEncapsulation.None + selector: 'tb-rule-node-config', + templateUrl: './rule-node-config.component.html', + styleUrls: ['./rule-node-config.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleNodeConfigComponent), + multi: true + }], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class RuleNodeConfigComponent implements ControlValueAccessor, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index 91af3024b6..b5cbfaf5a8 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -40,9 +40,10 @@ import { takeUntil } from 'rxjs/operators'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; @Component({ - selector: 'tb-rule-node', - templateUrl: './rule-node-details.component.html', - styleUrls: ['./rule-node-details.component.scss'] + selector: 'tb-rule-node', + templateUrl: './rule-node-details.component.html', + styleUrls: ['./rule-node-details.component.scss'], + standalone: false }) export class RuleNodeDetailsComponent extends PageComponent implements OnInit, OnChanges, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts index 14cd0a8190..e8ac36e3fb 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts @@ -23,14 +23,15 @@ import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-rule-node-link', - templateUrl: './rule-node-link.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => RuleNodeLinkComponent), - multi: true - }] + selector: 'tb-rule-node-link', + templateUrl: './rule-node-link.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleNodeLinkComponent), + multi: true + }], + standalone: false }) export class RuleNodeLinkComponent implements ControlValueAccessor, OnInit { 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 dfa196f2db..8707e42cac 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 @@ -103,10 +103,11 @@ import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entit import { EventsDialogComponent } from '@home/dialogs/events-dialog.component'; @Component({ - selector: 'tb-rulechain-page', - templateUrl: './rulechain-page.component.html', - styleUrls: ['./rulechain-page.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-rulechain-page', + templateUrl: './rulechain-page.component.html', + styleUrls: ['./rulechain-page.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class RuleChainPageComponent extends PageComponent implements AfterViewInit, OnInit, OnDestroy, HasDirtyFlag, ISearchableComponent, AfterViewChecked { @@ -1788,10 +1789,11 @@ export interface AddRuleNodeLinkDialogData { } @Component({ - selector: 'tb-add-rule-node-link-dialog', - templateUrl: './add-rule-node-link-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AddRuleNodeLinkDialogComponent}], - styleUrls: ['./add-rule-node-link-dialog.component.scss'] + selector: 'tb-add-rule-node-link-dialog', + templateUrl: './add-rule-node-link-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AddRuleNodeLinkDialogComponent }], + styleUrls: ['./add-rule-node-link-dialog.component.scss'], + standalone: false }) export class AddRuleNodeLinkDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { @@ -1852,10 +1854,11 @@ export interface AddRuleNodeDialogData { } @Component({ - selector: 'tb-add-rule-node-dialog', - templateUrl: './add-rule-node-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AddRuleNodeDialogComponent}], - styleUrls: ['./add-rule-node-dialog.component.scss'] + selector: 'tb-add-rule-node-dialog', + templateUrl: './add-rule-node-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: AddRuleNodeDialogComponent }], + styleUrls: ['./add-rule-node-dialog.component.scss'], + standalone: false }) export class AddRuleNodeDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { @@ -1912,10 +1915,11 @@ export interface CreateNestedRuleChainDialogData { } @Component({ - selector: 'tb-create-nested-rulechain-dialog', - templateUrl: './create-nested-rulechain-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: CreateNestedRuleChainDialogComponent}], - styleUrls: [] + selector: 'tb-create-nested-rulechain-dialog', + templateUrl: './create-nested-rulechain-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: CreateNestedRuleChainDialogComponent }], + styleUrls: [], + standalone: false }) export class CreateNestedRuleChainDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.ts index 0ec3c4df52..8e26eed483 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.ts @@ -21,9 +21,10 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone import { RuleChain } from '@shared/models/rule-chain.models'; @Component({ - selector: 'tb-rulechain-tabs', - templateUrl: './rulechain-tabs.component.html', - styleUrls: [] + selector: 'tb-rulechain-tabs', + templateUrl: './rulechain-tabs.component.html', + styleUrls: [], + standalone: false }) export class RuleChainTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts index df55eeff90..5769009066 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts @@ -25,9 +25,10 @@ import { RuleChain } from '@shared/models/rule-chain.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; @Component({ - selector: 'tb-rulechain', - templateUrl: './rulechain.component.html', - styleUrls: ['./rulechain.component.scss'] + selector: 'tb-rulechain', + templateUrl: './rulechain.component.html', + styleUrls: ['./rulechain.component.scss'], + standalone: false }) export class RuleChainComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts index 87e2d0a31b..04c293d241 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts @@ -23,10 +23,11 @@ import { RuleChainType } from '@app/shared/models/rule-chain.models'; import { TranslateService } from '@ngx-translate/core'; @Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'rule-node', - templateUrl: './rulenode.component.html', - styleUrls: ['./rulenode.component.scss'] + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'rule-node', + templateUrl: './rulenode.component.html', + styleUrls: ['./rulenode.component.scss'], + standalone: false }) export class RuleNodeComponent extends FcNodeComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts index c488258b6d..793c1307db 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts @@ -35,10 +35,11 @@ import { WidgetActionCallbacks } from '@home/components/widget/action/manage-wid import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-scada-symbol-behavior-panel', - templateUrl: './scada-symbol-behavior-panel.component.html', - styleUrls: ['./scada-symbol-behavior-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol-behavior-panel', + templateUrl: './scada-symbol-behavior-panel.component.html', + styleUrls: ['./scada-symbol-behavior-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolBehaviorPanelComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts index 2d53986923..18b3d3bf4c 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts @@ -91,22 +91,23 @@ export const behaviorValid = (behavior: ScadaSymbolBehavior): boolean => { }; @Component({ - selector: 'tb-scada-symbol-metadata-behavior-row', - templateUrl: './scada-symbol-behavior-row.component.html', - styleUrls: ['./scada-symbol-behavior-row.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol-metadata-behavior-row', + templateUrl: './scada-symbol-behavior-row.component.html', + styleUrls: ['./scada-symbol-behavior-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts index 9775ad9a63..bb24f86a19 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts @@ -55,22 +55,23 @@ import { WidgetActionCallbacks } from '@home/components/widget/action/manage-wid import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-scada-symbol-metadata-behaviors', - templateUrl: './scada-symbol-behaviors.component.html', - styleUrls: ['./scada-symbol-behaviors.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol-metadata-behaviors', + templateUrl: './scada-symbol-behaviors.component.html', + styleUrls: ['./scada-symbol-behaviors.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolBehaviorsComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts index 3f9cdddd7c..2e1a7e4d29 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts @@ -36,10 +36,11 @@ import { import { JsFuncComponent } from '@shared/components/js-func.component'; @Component({ - selector: 'tb-scada-symbol-metadata-tag-function-panel', - templateUrl: './scada-symbol-metadata-tag-function-panel.component.html', - styleUrls: ['./scada-symbol-metadata-tag-function-panel.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol-metadata-tag-function-panel', + templateUrl: './scada-symbol-metadata-tag-function-panel.component.html', + styleUrls: ['./scada-symbol-metadata-tag-function-panel.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts index 352d3823da..a1237d87dc 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts @@ -43,22 +43,23 @@ import { } from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component'; @Component({ - selector: 'tb-scada-symbol-metadata-tag', - templateUrl: './scada-symbol-metadata-tag.component.html', - styleUrls: ['./scada-symbol-metadata-tag.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ScadaSymbolMetadataTagComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ScadaSymbolMetadataTagComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol-metadata-tag', + templateUrl: './scada-symbol-metadata-tag.component.html', + styleUrls: ['./scada-symbol-metadata-tag.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolMetadataTagComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolMetadataTagComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, OnInit, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts index d4286b762c..f2556d3cf0 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts @@ -49,22 +49,23 @@ const tagIsEmpty = (tag: ScadaSymbolTag): boolean => !tag.stateRenderFunction && !tag.actions?.click?.actionFunction; @Component({ - selector: 'tb-scada-symbol-metadata-tags', - templateUrl: './scada-symbol-metadata-tags.component.html', - styleUrls: ['./scada-symbol-metadata-tags.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol-metadata-tags', + templateUrl: './scada-symbol-metadata-tags.component.html', + styleUrls: ['./scada-symbol-metadata-tags.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, OnInit, Validator, OnChanges { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index 9dd2afba52..a893100791 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -58,22 +58,23 @@ import { WidgetActionCallbacks } from '@home/components/widget/action/manage-wid import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ - selector: 'tb-scada-symbol-metadata', - templateUrl: './scada-symbol-metadata.component.html', - styleUrls: ['./scada-symbol-metadata.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ScadaSymbolMetadataComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ScadaSymbolMetadataComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol-metadata', + templateUrl: './scada-symbol-metadata.component.html', + styleUrls: ['./scada-symbol-metadata.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolMetadataComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolMetadataComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolMetadataComponent extends PageComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts index 6ba0866d89..16380ea570 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts @@ -50,10 +50,11 @@ export interface ScadaSymbolEditorData { type editorModeType = 'svg' | 'xml'; @Component({ - selector: 'tb-scada-symbol-editor', - templateUrl: './scada-symbol-editor.component.html', - styleUrls: ['./scada-symbol-editor.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol-editor', + templateUrl: './scada-symbol-editor.component.html', + styleUrls: ['./scada-symbol-editor.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewInit, OnChanges { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts index e8558c5206..60baf0e63b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -57,15 +57,16 @@ abstract class ScadaSymbolPanelComponent implements AfterViewInit { } @Component({ - template: `
+ template: `
{{ symbolElement?.element?.type }}{{ symbolElement?.invisible ? ' (' + ('scada.hidden' | translate) + ')' : '' }}
`, - styleUrls: ['./scada-symbol-tooltip.component.scss'], - encapsulation: ViewEncapsulation.None + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) class ScadaSymbolAddTagPanelComponent extends ScadaSymbolPanelComponent { @@ -83,7 +84,7 @@ class ScadaSymbolAddTagPanelComponent extends ScadaSymbolPanelComponent { } @Component({ - template: `
+ template: `
{{ (isAdd ? 'scada.tag.enter-tag' : 'scada.tag.update-tag' ) | translate }}: close
`, - styleUrls: ['./scada-symbol-tooltip.component.scss'], - encapsulation: ViewEncapsulation.None + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implements AfterViewInit, OnDestroy { @@ -181,7 +183,7 @@ class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implem } @Component({ - template: `
+ template: `
{{ symbolElement?.element?.type }}{{ symbolElement?.invisible ? ' (' + ('scada.hidden' | translate) + ')' : '' }}: {{ symbolElement?.tag }}
`, - styleUrls: ['./scada-symbol-tooltip.component.scss'], - encapsulation: ViewEncapsulation.None + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { @@ -305,7 +308,7 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements } @Component({ - template: `
+ template: `
`, - styleUrls: ['./scada-symbol-tooltip.component.scss'], - encapsulation: ViewEncapsulation.None + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { @@ -355,7 +359,7 @@ class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent imp } @Component({ - template: `
+ template: `
scada.state-render-function
`, - styleUrls: ['./scada-symbol-tooltip.component.scss'], - encapsulation: ViewEncapsulation.None + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) class ScadaSymbolTagSettingsComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts index 2c04591850..f2a358fde7 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -82,10 +82,11 @@ import { WidgetService } from '@core/http/widget.service'; import { ActionNotificationShow } from '@core/notification/notification.actions'; @Component({ - selector: 'tb-scada-symbol', - templateUrl: './scada-symbol.component.html', - styleUrls: ['./scada-symbol.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-scada-symbol', + templateUrl: './scada-symbol.component.html', + styleUrls: ['./scada-symbol.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ScadaSymbolComponent extends PageComponent implements OnInit, OnDestroy, HasDirtyFlag, ScadaSymbolEditObjectCallbacks { diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts index 121aac9ee3..e785b6b538 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts @@ -34,9 +34,10 @@ import { deepClone } from '@core/utils'; import printTemplate from './backup-code-print-template.raw'; @Component({ - selector: 'tb-backup-code-auth-dialog', - templateUrl: './backup-code-auth-dialog.component.html', - styleUrls: ['./authentication-dialog.component.scss'] + selector: 'tb-backup-code-auth-dialog', + templateUrl: './backup-code-auth-dialog.component.html', + styleUrls: ['./authentication-dialog.component.scss'], + standalone: false }) export class BackupCodeAuthDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts index 055298aa3c..dee27ba294 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts @@ -35,9 +35,10 @@ export interface EmailAuthDialogData { } @Component({ - selector: 'tb-email-auth-dialog', - templateUrl: './email-auth-dialog.component.html', - styleUrls: ['./authentication-dialog.component.scss'] + selector: 'tb-email-auth-dialog', + templateUrl: './email-auth-dialog.component.html', + styleUrls: ['./authentication-dialog.component.scss'], + standalone: false }) export class EmailAuthDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts index 63b759f79f..952bb21211 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts @@ -31,9 +31,10 @@ import { phoneNumberPattern } from '@shared/models/settings.models'; import { MatStepper } from '@angular/material/stepper'; @Component({ - selector: 'tb-sms-auth-dialog', - templateUrl: './sms-auth-dialog.component.html', - styleUrls: ['./authentication-dialog.component.scss'] + selector: 'tb-sms-auth-dialog', + templateUrl: './sms-auth-dialog.component.html', + styleUrls: ['./authentication-dialog.component.scss'], + standalone: false }) export class SMSAuthDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts index bb4fc921cb..fe394393e7 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts @@ -31,9 +31,10 @@ import { MatStepper } from '@angular/material/stepper'; import { unwrapModule } from '@core/utils'; @Component({ - selector: 'tb-totp-auth-dialog', - templateUrl: './totp-auth-dialog.component.html', - styleUrls: ['./authentication-dialog.component.scss'] + selector: 'tb-totp-auth-dialog', + templateUrl: './totp-auth-dialog.component.html', + styleUrls: ['./authentication-dialog.component.scss'], + standalone: false }) export class TotpAuthDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.ts b/ui-ngx/src/app/modules/home/pages/security/security.component.ts index 25eb42e673..60a8c54e0d 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.ts @@ -59,9 +59,10 @@ import { import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models'; @Component({ - selector: 'tb-security', - templateUrl: './security.component.html', - styleUrls: ['./security.component.scss'] + selector: 'tb-security', + templateUrl: './security.component.html', + styleUrls: ['./security.component.scss'], + standalone: false }) export class SecurityComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts index e034ad9b7f..0eb66ec279 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts @@ -21,9 +21,10 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone import { TenantProfile } from '@shared/models/tenant.model'; @Component({ - selector: 'tb-tenant-profile-tabs', - templateUrl: './tenant-profile-tabs.component.html', - styleUrls: [] + selector: 'tb-tenant-profile-tabs', + templateUrl: './tenant-profile-tabs.component.html', + styleUrls: [], + standalone: false }) export class TenantProfileTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts index 39d31a5367..8e0738b848 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.ts @@ -21,9 +21,10 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone import { TenantInfo } from '@shared/models/tenant.model'; @Component({ - selector: 'tb-tenant-tabs', - templateUrl: './tenant-tabs.component.html', - styleUrls: [] + selector: 'tb-tenant-tabs', + templateUrl: './tenant-tabs.component.html', + styleUrls: [], + standalone: false }) export class TenantTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts index 9ce2817e00..bfac5322f4 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts @@ -27,9 +27,10 @@ import { isDefinedAndNotNull } from '@core/utils'; import { CountryData } from '@shared/models/country.models'; @Component({ - selector: 'tb-tenant', - templateUrl: './tenant.component.html', - styleUrls: ['./tenant.component.scss'] + selector: 'tb-tenant', + templateUrl: './tenant.component.html', + styleUrls: ['./tenant.component.scss'], + standalone: false }) export class TenantComponent extends ContactBasedComponent { diff --git a/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts index ec869655c7..ead57fc0bf 100644 --- a/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts @@ -30,8 +30,9 @@ export interface ActivationLinkDialogData { } @Component({ - selector: 'tb-activation-link-dialog', - templateUrl: './activation-link-dialog.component.html' + selector: 'tb-activation-link-dialog', + templateUrl: './activation-link-dialog.component.html', + standalone: false }) export class ActivationLinkDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts index ed39e2b326..c8b7aabdac 100644 --- a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts @@ -40,9 +40,10 @@ export interface AddUserDialogData { } @Component({ - selector: 'tb-add-user-dialog', - templateUrl: './add-user-dialog.component.html', - styleUrls: ['./add-user-dialog.component.scss'] + selector: 'tb-add-user-dialog', + templateUrl: './add-user-dialog.component.html', + styleUrls: ['./add-user-dialog.component.scss'], + standalone: false }) export class AddUserDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.ts index 0180d79f79..298af6f0de 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.ts @@ -21,9 +21,10 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone import { User } from '@app/shared/models/user.model'; @Component({ - selector: 'tb-user-tabs', - templateUrl: './user-tabs.component.html', - styleUrls: [] + selector: 'tb-user-tabs', + templateUrl: './user-tabs.component.html', + styleUrls: [], + standalone: false }) export class UserTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/user/user.component.ts b/ui-ngx/src/app/modules/home/pages/user/user.component.ts index bea553fae0..3a60b33208 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user.component.ts +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.ts @@ -31,9 +31,10 @@ import { environment as env } from '@env/environment'; import { UnitSystems } from '@shared/models/unit.models'; @Component({ - selector: 'tb-user', - templateUrl: './user.component.html', - styleUrls: ['./user.component.scss'] + selector: 'tb-user', + templateUrl: './user.component.html', + styleUrls: ['./user.component.scss'], + standalone: false }) export class UserComponent extends EntityComponent{ diff --git a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts index 8c6e8fe9d4..c0522ba993 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts @@ -36,9 +36,10 @@ export interface SaveWidgetTypeAsDialogData { } @Component({ - selector: 'tb-save-widget-type-as-dialog', - templateUrl: './save-widget-type-as-dialog.component.html', - styleUrls: [] + selector: 'tb-save-widget-type-as-dialog', + templateUrl: './save-widget-type-as-dialog.component.html', + styleUrls: [], + standalone: false }) export class SaveWidgetTypeAsDialogComponent extends DialogComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts index c2c7195003..aec29b4ecd 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts @@ -23,9 +23,10 @@ import { Router } from '@angular/router'; import { widgetType, widgetTypesData } from '@shared/models/widget.models'; @Component({ - selector: 'tb-select-widget-type-dialog', - templateUrl: './select-widget-type-dialog.component.html', - styleUrls: ['./select-widget-type-dialog.component.scss'] + selector: 'tb-select-widget-type-dialog', + templateUrl: './select-widget-type-dialog.component.html', + styleUrls: ['./select-widget-type-dialog.component.scss'], + standalone: false }) export class SelectWidgetTypeDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index 098cfce2f6..8504cc01d5 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -78,10 +78,11 @@ import Timeout = NodeJS.Timeout; // @dynamic @Component({ - selector: 'tb-widget-editor', - templateUrl: './widget-editor.component.html', - styleUrls: ['./widget-editor.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-editor', + templateUrl: './widget-editor.component.html', + styleUrls: ['./widget-editor.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetEditorComponent extends PageComponent implements OnInit, OnDestroy, HasDirtyFlag { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts index e309e21d1d..04fd5af1cb 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts @@ -37,15 +37,16 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { WidgetService } from '@core/http/widget.service'; @Component({ - selector: 'tb-widget-type-autocomplete', - templateUrl: './widget-type-autocomplete.component.html', - styleUrls: ['./widget-type-autocomplete.component.scss'], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => WidgetTypeAutocompleteComponent), - multi: true - }], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-type-autocomplete', + templateUrl: './widget-type-autocomplete.component.html', + styleUrls: ['./widget-type-autocomplete.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetTypeAutocompleteComponent), + multi: true + }], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.ts index acee3159d2..0168bfb639 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.ts @@ -22,9 +22,10 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { WidgetTypeDetails } from '@shared/models/widget.models'; @Component({ - selector: 'tb-widget-type-tabs', - templateUrl: './widget-type-tabs.component.html', - styleUrls: [] + selector: 'tb-widget-type-tabs', + templateUrl: './widget-type-tabs.component.html', + styleUrls: [], + standalone: false }) export class WidgetTypeTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts index b765fadb1d..c6911e287e 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts @@ -24,9 +24,10 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod import { WidgetTypeDetails } from '@shared/models/widget.models'; @Component({ - selector: 'tb-widget-type', - templateUrl: './widget-type.component.html', - styleUrls: [] + selector: 'tb-widget-type', + templateUrl: './widget-type.component.html', + styleUrls: [], + standalone: false }) export class WidgetTypeComponent extends EntityComponent { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.ts index 5e2e4e0c4d..c493ba032d 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.ts @@ -31,10 +31,11 @@ export interface WidgetsBundleDialogData { } @Component({ - selector: 'tb-widgets-bundle-dialog', - templateUrl: './widgets-bundle-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: WidgetsBundleDialogComponent}], - styleUrls: ['widgets-bundle-dialog.component.scss'] + selector: 'tb-widgets-bundle-dialog', + templateUrl: './widgets-bundle-dialog.component.html', + providers: [{ provide: ErrorStateMatcher, useExisting: WidgetsBundleDialogComponent }], + styleUrls: ['widgets-bundle-dialog.component.scss'], + standalone: false }) export class WidgetsBundleDialogComponent extends DialogComponent implements ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-tabs.component.ts index 5a684bc031..6e569ff701 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-tabs.component.ts @@ -22,9 +22,10 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { NULL_UUID } from '@shared/models/id/has-uuid'; @Component({ - selector: 'tb-widgets-bundle-tabs', - templateUrl: './widgets-bundle-tabs.component.html', - styleUrls: [] + selector: 'tb-widgets-bundle-tabs', + templateUrl: './widgets-bundle-tabs.component.html', + styleUrls: [], + standalone: false }) export class WidgetsBundleTabsComponent extends EntityTabsComponent { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts index 7eb3afddc6..50cf6b9ef7 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts @@ -36,9 +36,10 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; type WidgetTypeBundle = WithOptional; @Component({ - selector: 'tb-widgets-bundle-widget', - templateUrl: './widgets-bundle-widgets.component.html', - styleUrls: ['./widgets-bundle-widgets.component.scss'] + selector: 'tb-widgets-bundle-widget', + templateUrl: './widgets-bundle-widgets.component.html', + styleUrls: ['./widgets-bundle-widgets.component.scss'], + standalone: false }) export class WidgetsBundleWidgetsComponent extends PageComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts index 1ccabc8050..bbfe6cb90e 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts @@ -23,9 +23,10 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; @Component({ - selector: 'tb-widgets-bundle', - templateUrl: './widgets-bundle.component.html', - styleUrls: ['./widgets-bundle.component.scss'] + selector: 'tb-widgets-bundle', + templateUrl: './widgets-bundle.component.html', + styleUrls: ['./widgets-bundle.component.scss'], + standalone: false }) export class WidgetsBundleComponent extends EntityComponent { 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 03103ae6c3..c72f1751a2 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 @@ -24,9 +24,10 @@ import { finalize } from 'rxjs/operators'; import { PageComponent } from '@shared/components/page.component'; @Component({ - selector: 'tb-create-password', - templateUrl: './create-password.component.html', - styleUrls: ['./password.component.scss'] + selector: 'tb-create-password', + templateUrl: './create-password.component.html', + styleUrls: ['./password.component.scss'], + standalone: false }) export class CreatePasswordComponent extends PageComponent { diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts index c5e02c19da..364ce7ffac 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts @@ -61,9 +61,10 @@ enum BackupCodeState { } @Component({ - selector: 'tb-force-two-factor-auth-login', - templateUrl: './force-two-factor-auth-login.component.html', - styleUrls: ['./force-two-factor-auth-login.component.scss'] + selector: 'tb-force-two-factor-auth-login', + templateUrl: './force-two-factor-auth-login.component.html', + styleUrls: ['./force-two-factor-auth-login.component.scss'], + standalone: false }) export class ForceTwoFactorAuthLoginComponent extends PageComponent implements OnInit, OnDestroy { 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 5bc00c565e..1e4f2ec68d 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 @@ -18,9 +18,10 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ - selector: 'tb-link-expired', - templateUrl: './link-expired.component.html', - styleUrls: ['./link-expired.component.scss'] + selector: 'tb-link-expired', + templateUrl: './link-expired.component.html', + styleUrls: ['./link-expired.component.scss'], + standalone: false }) export class LinkExpiredComponent { 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 e49309298d..1cf115f9dc 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 @@ -26,9 +26,10 @@ import { PageComponent } from '@shared/components/page.component'; import { finalize } from 'rxjs/operators'; @Component({ - selector: 'tb-login', - templateUrl: './login.component.html', - styleUrls: ['./login.component.scss'] + selector: 'tb-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], + standalone: false }) export class LoginComponent extends PageComponent implements OnInit { 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 b1513105a1..716b761d3d 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 @@ -25,9 +25,10 @@ import { TranslateService } from '@ngx-translate/core'; import { validateEmail } from '@app/core/utils'; @Component({ - selector: 'tb-reset-password-request', - templateUrl: './reset-password-request.component.html', - styleUrls: ['./password.component.scss'] + selector: 'tb-reset-password-request', + templateUrl: './reset-password-request.component.html', + styleUrls: ['./password.component.scss'], + standalone: false }) export class ResetPasswordRequestComponent extends PageComponent { 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 23e775f085..fb6a679ab0 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 @@ -23,9 +23,10 @@ import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/mode import { finalize } from 'rxjs/operators'; @Component({ - selector: 'tb-reset-password', - templateUrl: './reset-password.component.html', - styleUrls: ['./password.component.scss'] + selector: 'tb-reset-password', + templateUrl: './reset-password.component.html', + styleUrls: ['./password.component.scss'], + standalone: false }) export class ResetPasswordComponent { diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts index b4b2e896ac..856374fe27 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts @@ -32,9 +32,10 @@ import { isEqual } from '@core/utils'; import { ActionNotificationShow } from '@core/notification/notification.actions'; @Component({ - selector: 'tb-two-factor-auth-login', - templateUrl: './two-factor-auth-login.component.html', - styleUrls: ['./two-factor-auth-login.component.scss'] + selector: 'tb-two-factor-auth-login', + templateUrl: './two-factor-auth-login.component.html', + styleUrls: ['./two-factor-auth-login.component.scss'], + standalone: false }) export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index 858bd20370..75603fc9cb 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -28,10 +28,11 @@ import { MenuSection, menuSectionMap } from '@core/services/menu.models'; import { MenuService } from '@core/services/menu.service'; @Component({ - selector: 'tb-breadcrumb', - templateUrl: './breadcrumb.component.html', - styleUrls: ['./breadcrumb.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'tb-breadcrumb', + templateUrl: './breadcrumb.component.html', + styleUrls: ['./breadcrumb.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class BreadcrumbComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/shared/components/button/copy-button.component.ts b/ui-ngx/src/app/shared/components/button/copy-button.component.ts index 6f2573ccbd..9f5617a4da 100644 --- a/ui-ngx/src/app/shared/components/button/copy-button.component.ts +++ b/ui-ngx/src/app/shared/components/button/copy-button.component.ts @@ -22,9 +22,10 @@ import { ThemePalette } from '@angular/material/core'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-copy-button', - styleUrls: ['copy-button.component.scss'], - templateUrl: './copy-button.component.html' + selector: 'tb-copy-button', + styleUrls: ['copy-button.component.scss'], + templateUrl: './copy-button.component.html', + standalone: false }) export class CopyButtonComponent { diff --git a/ui-ngx/src/app/shared/components/button/toggle-password.component.ts b/ui-ngx/src/app/shared/components/button/toggle-password.component.ts index 28ba243e1e..b3a1ca1e70 100644 --- a/ui-ngx/src/app/shared/components/button/toggle-password.component.ts +++ b/ui-ngx/src/app/shared/components/button/toggle-password.component.ts @@ -17,9 +17,10 @@ import { AfterViewInit, Component, ElementRef } from '@angular/core'; @Component({ - selector: 'tb-toggle-password', - templateUrl: 'toggle-password.component.html', - styleUrls: [], + selector: 'tb-toggle-password', + templateUrl: 'toggle-password.component.html', + styleUrls: [], + standalone: false }) export class TogglePasswordComponent implements AfterViewInit { showPassword = false; diff --git a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts index 62d3d9ff0c..488afa9a81 100644 --- a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts +++ b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts @@ -46,9 +46,10 @@ const initialButtonHeight = 60; const horizontalLayoutPadding = 10; @Component({ - selector: 'tb-widget-button-toggle', - templateUrl: './widget-button-toggle.component.html', - styleUrls: ['./widget-button-toggle.component.scss'] + selector: 'tb-widget-button-toggle', + templateUrl: './widget-button-toggle.component.html', + styleUrls: ['./widget-button-toggle.component.scss'], + standalone: false }) export class WidgetButtonToggleComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { diff --git a/ui-ngx/src/app/shared/components/button/widget-button.component.ts b/ui-ngx/src/app/shared/components/button/widget-button.component.ts index 213b5c2a47..7c4ef584d5 100644 --- a/ui-ngx/src/app/shared/components/button/widget-button.component.ts +++ b/ui-ngx/src/app/shared/components/button/widget-button.component.ts @@ -45,10 +45,11 @@ const horizontalLayoutPadding = 24; const verticalLayoutPadding = 16; @Component({ - selector: 'tb-widget-button', - templateUrl: './widget-button.component.html', - styleUrls: ['./widget-button.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'tb-widget-button', + templateUrl: './widget-button.component.html', + styleUrls: ['./widget-button.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class WidgetButtonComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { diff --git a/ui-ngx/src/app/shared/components/cheatsheet.component.ts b/ui-ngx/src/app/shared/components/cheatsheet.component.ts index 93ae5c87b1..00fe6e5790 100644 --- a/ui-ngx/src/app/shared/components/cheatsheet.component.ts +++ b/ui-ngx/src/app/shared/components/cheatsheet.component.ts @@ -20,8 +20,8 @@ import { MousetrapInstance } from 'mousetrap'; import Mousetrap from 'mousetrap'; @Component({ - selector : 'tb-hotkeys-cheatsheet', - styles : [` + selector: 'tb-hotkeys-cheatsheet', + styles: [` .tb-hotkeys-container { display: table !important; position: fixed; @@ -114,7 +114,7 @@ import Mousetrap from 'mousetrap'; font-size: 1.2em; } } `], - template : `
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html index eda38cfa13..d738e0447a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html @@ -34,7 +34,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html index b88cd297ce..f432533614 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html @@ -30,7 +30,7 @@
-
+
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html index ff3d709352..0cc88f5404 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html @@ -20,7 +20,7 @@
-
+
{{ icon }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html index c1caf24348..1b5b3f1992 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html @@ -39,10 +39,10 @@ widgets.persistent-table.method - + {{'widgets.persistent-table.method-error' | translate}} - + {{'widgets.persistent-table.white-space-error' | translate}} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html index 5a8715adf9..b54bc91b6f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html @@ -20,7 +20,7 @@
-
+
{{ icon }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html index a1d60da276..26835e857a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html @@ -97,7 +97,7 @@
{{ widgetButtonStateTranslationMap.get(state) | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html index 63f59f169c..421d518a90 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html @@ -27,7 +27,7 @@ warning - + widgets.maps.data-layer.tooltip-tag-actions
-
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index 0ad5814a81..ef80df1e9a 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -108,7 +108,7 @@ color="primary" *ngIf="showNext" [disabled]="(isLoading$ | async)" - (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }} + (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(selectedIndex+1) | translate)} }}
diff --git a/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html index 90fc24c15b..0b337f0bd9 100644 --- a/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html @@ -35,7 +35,7 @@ [entityId]="data.entityId" [functionTestButtonLabel]="data.functionTestButtonLabel ?? ''" [disableDebugEventAction]="data.debugActionDisabled" - (debugEventSelected)="this.onDebugEventSelected($event)" + (debugEventSelected)="onDebugEventSelected($event)" />
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html index 1def8334ad..4259899d46 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -89,7 +89,7 @@ labelText="{{ 'admin.oauth2.clients' | translate }}" placeholderText="{{ 'admin.oauth2.add-client' | translate }}"> diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html index 131ea43496..45478969f8 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html @@ -130,7 +130,7 @@
+ *ngIf="!(isAdd && entityForm.get('generateChecksum').value)"> ota-update.checksum-algorithm 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 703c710add..2d060483b1 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 @@ -39,7 +39,7 @@ formControlName="newPassword"/> lock - + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} 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 bbf159107f..a3ea2b75e5 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 @@ -36,7 +36,7 @@
-
diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index 07606331cf..a3ebd7c3f0 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -150,7 +150,7 @@
device.description - + diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html index 83f95c71e2..013cd4e52e 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html @@ -198,7 +198,7 @@
edge.description - +
diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html index 15b0b3da8b..87a114761b 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html @@ -161,7 +161,7 @@
entity-view.description - +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html index 4259899d46..92ba966aee 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -73,7 +73,7 @@ mobile.description - + diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html index 02e706264d..f7c56f63db 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html @@ -155,7 +155,7 @@
notification.description - + diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html index cc6e24e952..dda79636f1 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html @@ -150,7 +150,7 @@
notification.description - +
@@ -204,7 +204,7 @@
notification.description - +
@@ -233,7 +233,7 @@
notification.description - +
@@ -287,7 +287,7 @@
notification.description - +
@@ -343,7 +343,7 @@
notification.description - +
@@ -402,7 +402,7 @@
notification.description - +
@@ -439,7 +439,7 @@
notification.description - +
@@ -464,7 +464,7 @@
notification.description - +
@@ -501,7 +501,7 @@
notification.description - +
@@ -538,7 +538,7 @@
notification.description - +
@@ -551,7 +551,7 @@
notification.description - +
@@ -574,7 +574,7 @@
notification.description - +
@@ -587,7 +587,7 @@
notification.description - +
@@ -650,7 +650,7 @@
notification.description - +
diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html index 45478969f8..a90de8201c 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html @@ -177,7 +177,7 @@
ota-update.description - +
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 6afce790b6..48aa47f3ae 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 @@ -75,7 +75,7 @@
rulenode.rule-node-description - +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html index 403cbbb953..02afcfe886 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html @@ -94,7 +94,7 @@
rulechain.description - +
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html index 3d981e3611..30dbe4f734 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html @@ -37,8 +37,10 @@
scada.description
+ rows="1">
diff --git a/ui-ngx/src/app/modules/home/pages/user/user.component.html b/ui-ngx/src/app/modules/home/pages/user/user.component.html index 00a0944043..e2d9fec8c0 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.html @@ -119,10 +119,6 @@ } - - user.description - -
+ + user.description + +
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html index 871b9b37ef..b4721ea309 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html @@ -58,11 +58,6 @@ label="{{'widget.image-preview' | translate}}" formControlName="image"> - - widget.description - - {{descriptionInput.value?.length || 0}}/1024 - @@ -75,6 +70,11 @@ {{ 'widget.deprecated' | translate }} + + widget.description + + {{descriptionInput.value?.length || 0}}/1024 + From 4f37dace977bd605e6a707ff347881cd14ce2a83 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 6 Mar 2026 12:32:46 +0200 Subject: [PATCH 114/313] minor Swagger improvements --- .../org/thingsboard/server/config/SwaggerConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 654d89bc8e..12a1be5713 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -306,7 +306,7 @@ public class SwaggerConfiguration { } } else if (schema != null && schema.getProperties() != null && !schema.getProperties().isEmpty()) { try { - var beanDesc = JacksonUtil.OBJECT_MAPPER.getSerializationConfig().introspect(javaType); + var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType); var orderedNames = resolvePropertyOrder(cls, beanDesc); if (!orderedNames.isEmpty()) { @SuppressWarnings("unchecked") @@ -320,7 +320,7 @@ public class SwaggerConfiguration { schema.setProperties(reordered); } } catch (Exception ignored) { - // keep original order if introspection fails + log.trace("Failed to resolve property order for {}", cls.getName(), ignored); } } } From b9850d1f51d18d343363ab09a52f35c0172c4d2b Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 27 Feb 2026 11:05:58 +0200 Subject: [PATCH 115/313] UI: Implements rule chain note components --- ui-ngx/package.json | 2 +- .../app/core/services/item-buffer.service.ts | 40 ++- .../rulechain/add-note-dialog.component.html | 36 +++ .../rulechain/add-note-dialog.component.scss | 30 ++ .../pages/rulechain/rule-note.component.html | 46 +++ .../pages/rulechain/rule-note.component.ts | 57 ++++ .../rulechain/rulechain-page.component.html | 16 +- .../rulechain/rulechain-page.component.scss | 13 + .../rulechain/rulechain-page.component.ts | 288 +++++++++++++++--- .../pages/rulechain/rulechain-page.module.ts | 6 +- .../home/pages/rulechain/rulechain.module.ts | 10 +- .../pages/rulechain/rulenote.component.html | 47 +++ .../pages/rulechain/rulenote.component.scss | 71 +++++ .../pages/rulechain/rulenote.component.ts | 78 +++++ .../components/markdown-editor.component.html | 2 +- .../components/markdown-editor.component.ts | 2 + .../app/shared/models/rule-chain.models.ts | 3 +- .../src/app/shared/models/rule-node.models.ts | 10 +- .../help/en_US/rulechain/note_content.md | 195 ++++++++++++ .../assets/locale/locale.constant-en_US.json | 12 +- ui-ngx/yarn.lock | 3 +- 21 files changed, 909 insertions(+), 58 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.ts create mode 100644 ui-ngx/src/assets/help/en_US/rulechain/note_content.md diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 8642ecffaa..5b4d985114 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -71,7 +71,7 @@ "ngx-clipboard": "^16.0.0", "ngx-daterangepicker-material": "^6.0.4", "ngx-drag-drop": "^20.0.1", - "ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/4.0.0", + "ngx-flowchart": "file:../../ngx-flowchart/dist/ngx-flowchart", "ngx-hm-carousel": "^19.0.0", "ngx-markdown": "^20.1.0", "ngx-sharebuttons": "^17.0.0", diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index 7b7c828cf3..ba4b29cbcb 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -31,7 +31,7 @@ import { deepClone, isDefinedAndNotNull, isEqual } from '@core/utils'; import { UtilsService } from '@core/services/utils.service'; import { Observable, of, throwError } from 'rxjs'; import { map } from 'rxjs/operators'; -import { FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models'; +import { FcRuleNode, FcRuleNote, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models'; import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainImport } from '@shared/models/rule-chain.models'; import { Filter, FilterInfo, Filters, FiltersInfo, getFilterId } from '@shared/models/query/query.models'; @@ -72,6 +72,7 @@ export interface RuleNodeConnection { export interface RuleNodesReference { nodes: FcRuleNode[]; connections: RuleNodeConnection[]; + notes?: FcRuleNote[]; originX?: number; originY?: number; } @@ -312,10 +313,11 @@ export class ItemBufferService { return of(theDashboard); } - public copyRuleNodes(nodes: FcRuleNode[], connections: RuleNodeConnection[]) { + public copyRuleChainObjects(nodes: FcRuleNode[], connections: RuleNodeConnection[], notes: FcRuleNote[] = []) { const ruleNodes: RuleNodesReference = { nodes: [], - connections: [] + connections: [], + notes: [] }; let top = -1; let left = -1; @@ -364,6 +366,30 @@ export class ItemBufferService { right = Math.max(right, node.x + 170); } } + for (const origNote of notes) { + ruleNodes.notes.push({ + id: '', + x: origNote.x, + y: origNote.y, + width: origNote.width, + height: origNote.height, + content: origNote.content, + backgroundColor: origNote.backgroundColor, + applyDefaultMarkdownStyle: origNote.applyDefaultMarkdownStyle, + markdownCss: origNote.markdownCss + }); + if (top === -1) { + top = origNote.y; + left = origNote.x; + bottom = origNote.y + (origNote.height || 120); + right = origNote.x + (origNote.width || 200); + } else { + top = Math.min(top, origNote.y); + left = Math.min(left, origNote.x); + bottom = Math.max(bottom, origNote.y + (origNote.height || 120)); + right = Math.max(right, origNote.x + (origNote.width || 200)); + } + } ruleNodes.originX = left + (right - left) / 2; ruleNodes.originY = top + (bottom - top) / 2; connections.forEach(connection => { @@ -372,11 +398,11 @@ export class ItemBufferService { this.storeSet(RULE_NODES, ruleNodes); } - public hasRuleNodes(): boolean { + public hasRuleChainObjects(): boolean { return this.storeHas(RULE_NODES); } - public pasteRuleNodes(x: number, y: number): RuleNodesReference { + public pasteRuleChainObjects(x: number, y: number): RuleNodesReference { const ruleNodes: RuleNodesReference = this.storeGet(RULE_NODES); if (ruleNodes) { const deltaX = x - ruleNodes.originX; @@ -404,6 +430,10 @@ export class ItemBufferService { return null; } } + for (const note of (ruleNodes.notes || [])) { + note.x = Math.round(note.x + deltaX); + note.y = Math.round(note.y + deltaY); + } return ruleNodes; } return null; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html new file mode 100644 index 0000000000..5c98e69cd8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html @@ -0,0 +1,36 @@ + + +

rulechain.add-note

+ + +
+
+ + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss new file mode 100644 index 0000000000..fa37306f7b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + display: grid; + width: 650px; + height: 100%; + max-width: 100%; + max-height: 100vh; + grid-template-rows: min-content minmax(auto, 1fr) min-content; +} + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + .mat-mdc-dialog-content { + padding: 8px; + } +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.html new file mode 100644 index 0000000000..dcaea7550f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.html @@ -0,0 +1,46 @@ + +
+ + +
+ + + rule-node-config.advanced-settings + +
+
{{ 'rulechain.note-background-color' | translate }}
+ + +
+
+ + {{ 'rulechain.note-apply-default-markdown-style' | translate }} + +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.ts new file mode 100644 index 0000000000..37dceb3c03 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.ts @@ -0,0 +1,57 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, inject, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { FcRuleNote } from '@shared/models/rule-node.models'; + +@Component({ + selector: 'tb-rule-note', + templateUrl: './rule-note.component.html', + standalone: false +}) +export class RuleNoteComponent implements OnChanges { + + @Input() + note: FcRuleNote; + + private fb = inject(FormBuilder) + + noteForm = this.fb.group({ + content: [''], + backgroundColor: ['#FFF9C4'], + applyDefaultMarkdownStyle: [true], + markdownCss: [''] + }); + + ngOnChanges(changes: SimpleChanges): void { + if (changes.note) { + this.updatedForm(); + } + } + + private updatedForm(): void { + this.noteForm.setValue({ + content: this.note?.content || '', + backgroundColor: this.note?.backgroundColor || '#FFF9C4', + applyDefaultMarkdownStyle: this.note?.applyDefaultMarkdownStyle ?? true, + markdownCss: this.note?.markdownCss || '' + }) + setTimeout(() => { + this.noteForm.markAsPristine(); + }, 0); + } +} 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 9d855647fa..849084fee9 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 @@ -84,7 +84,7 @@ @@ -158,6 +158,20 @@ [sourceRuleChainId]="editingRuleNodeSourceRuleChainId"> + + + + -
+
; connections: Array; + notes?: Array; } export interface RuleChainImport { diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 30e1648f7e..84f88ccbfa 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -18,7 +18,7 @@ import { BaseData } from '@shared/models/base-data'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { RuleNodeId } from '@shared/models/id/rule-node-id'; import { ComponentDescriptor } from '@shared/models/component-descriptor.models'; -import { FcEdge, FcNode } from 'ngx-flowchart'; +import { FcEdge, FcNode, FcNote } from 'ngx-flowchart'; import { Observable } from 'rxjs'; import { PageComponent } from '@shared/components/page.component'; import { AfterViewInit, DestroyRef, Directive, EventEmitter, inject, OnInit } from '@angular/core'; @@ -27,6 +27,7 @@ import { RuleChainType } from '@shared/models/rule-chain.models'; import { DebugRuleNodeEventBody } from '@shared/models/event.models'; import { EntityTestScriptResult, HasEntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { strings } from '@angular-devkit/core'; export interface RuleNodeConfiguration { [key: string]: any; @@ -358,6 +359,13 @@ export interface FcRuleEdge extends FcEdge { labels?: string[]; } +export interface FcRuleNote extends FcNote { + content?: string; + backgroundColor?: string; + applyDefaultMarkdownStyle?: boolean; + markdownCss?: string; +} + export enum ScriptLanguage { JS = 'JS', TBEL = 'TBEL' diff --git a/ui-ngx/src/assets/help/en_US/rulechain/note_content.md b/ui-ngx/src/assets/help/en_US/rulechain/note_content.md new file mode 100644 index 0000000000..d21167be7f --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulechain/note_content.md @@ -0,0 +1,195 @@ +#### Markdown/HTML note content + +
+
+ +Notes support **Markdown** and **HTML** markup for rich text formatting directly on the rule chain canvas. + +
+
+ +#### Headings + +Use `#` to create headings. The number of `#` characters defines the level: + +```markdown +# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +{:copy-code} +``` + +
+
+ +#### Text formatting + +```markdown +**Bold text** +*Italic text* +~~Strikethrough~~ +**_Bold and italic_** +{:copy-code} +``` + +**Bold text** +*Italic text* +~~Strikethrough~~ +**_Bold and italic_** + +
+
+ +#### Lists + +Unordered list using `-` or `*`: + +```markdown +- Step 1: Receive telemetry +- Step 2: Filter by threshold + - Value > 100 → alarm branch + - Value ≤ 100 → success branch +- Step 3: Save to time series +{:copy-code} +``` + +- Step 1: Receive telemetry +- Step 2: Filter by threshold + - Value > 100 → alarm branch + - Value ≤ 100 → success branch +- Step 3: Save to time series + +Ordered list using numbers: + +```markdown +1. Validate message type +2. Enrich with device attributes +3. Route to appropriate handler +{:copy-code} +``` + +1. Validate message type +2. Enrich with device attributes +3. Route to appropriate handler + +
+
+ +#### Blockquotes + +Use `>` for notes, warnings, or highlighted information: + +```markdown +> **Note:** This node requires a valid device alias to be configured upstream. + +> **Warning:** Debug mode increases load — disable in production. +{:copy-code} +``` + +> **Note:** This node requires a valid device alias to be configured upstream. + +> **Warning:** Debug mode increases load — disable in production. + +
+
+ +#### Inline code and code blocks + +Use backticks for inline code and triple backticks for blocks: + +```markdown +The message type must be `POST_TELEMETRY_REQUEST`. +{:copy-code} +``` + +The message type must be `POST_TELEMETRY_REQUEST`. + +````markdown +```json +{ + "temperature": 42.5, + "humidity": 68 +} +``` +{:copy-code} +```` + +```json +{ + "temperature": 42.5, + "humidity": 68 +} +``` + +
+
+ +#### Tables + +```markdown +| Branch | Condition | Next node | +|----------|------------------|--------------------| +| True | temperature > 80 | Create Alarm | +| False | temperature ≤ 80 | Save Timeseries | +{:copy-code} +``` + +| Branch | Condition | Next node | +|----------|------------------|--------------------| +| True | temperature > 80 | Create Alarm | +| False | temperature ≤ 80 | Save Timeseries | + +
+
+ +#### Horizontal rule + +Use `---` to visually separate sections: + +```markdown +### Input + +Device telemetry message. + +--- + +### Output + +Enriched message with customer attributes. +{:copy-code} +``` + +
+
+ +#### HTML elements + +When **Apply default markdown style** is enabled, you can also use HTML tags: + +```html +🔴 Critical path
+🟢 Happy path +{:copy-code} +``` + +
+
+ +#### Tips for rule chain notes + +Use notes to annotate the canvas with context that helps your team: + +```markdown +## 🌡️ Temperature monitoring flow + +Processes telemetry from HVAC sensors. + +| Threshold | Severity | +|-----------|----------| +| > 85 °C | CRITICAL | +| > 70 °C | WARNING | + +> **Owner:** Platform team · **Updated:** 2026-02 +{:copy-code} +``` 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 5c527c9698..ff3a9cd639 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5165,7 +5165,17 @@ "unset-auto-assign-to-edge-title": "Are you sure you do not want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", "unset-auto-assign-to-edge-text": "After the confirmation the edge rule chain will no longer be automatically assigned to edge(s) on creation.", "unassign-rulechain-title": "Are you sure you want to unassign the rulechain '{{ruleChainName}}'?", - "unassign-rulechains": "Unassign rulechains" + "unassign-rulechains": "Unassign rulechains", + "add-note": "Add note", + "paste-note": "Paste note", + "note": "Note", + "edit-note": "Edit note", + "note-content": "Markdown/HTML content", + "note-details": "Note details", + "note-empty": "Empty note", + "note-background-color": "Background color", + "note-apply-default-markdown-style": "Apply default markdown style", + "note-custom-css": "Note content CSS" }, "rulenode": { "rule-node-events": "Rule node events", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 82fff6fbba..84e8586a4a 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -7989,9 +7989,8 @@ ngx-drag-drop@^20.0.1: dependencies: tslib "^2.3.0" -"ngx-flowchart@https://github.com/thingsboard/ngx-flowchart.git#release/4.0.0": +"ngx-flowchart@file:../../ngx-flowchart/dist/ngx-flowchart": version "4.0.0" - resolved "https://github.com/thingsboard/ngx-flowchart.git#735bc818ef218a169ac50e87edc6a093b3e97715" dependencies: tslib "^2.3.0" From db87b489f16c6b17df737a0656da5a0c60589bc9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 27 Feb 2026 18:31:09 +0200 Subject: [PATCH 116/313] Implements support notes in RuleChain entity --- .../main/data/upgrade/basic/schema_update.sql | 6 ++ .../server/common/data/rule/RuleChain.java | 4 ++ .../common/data/rule/RuleChainMetaData.java | 5 ++ .../common/data/rule/RuleChainNote.java | 58 +++++++++++++++++++ .../server/dao/model/ModelConstants.java | 1 + .../server/dao/model/sql/RuleChainEntity.java | 5 ++ .../server/dao/rule/BaseRuleChainService.java | 7 +++ .../main/resources/sql/schema-entities.sql | 1 + 8 files changed, 87 insertions(+) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index a2dfeac358..1d4b4ec393 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,3 +14,9 @@ -- limitations under the License. -- + +-- RULE CHAIN NOTES START + +ALTER TABLE rule_chain ADD COLUMN IF NOT EXISTS notes text; + +-- RULE CHAIN NOTES END diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index 4d8bb35919..444dca319d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -61,6 +61,9 @@ public class RuleChain extends BaseDataWithAdditionalInfo implement private RuleChainId externalId; private Long version; + @JsonIgnore + private String notes; + @JsonIgnore private byte[] configurationBytes; @@ -82,6 +85,7 @@ public class RuleChain extends BaseDataWithAdditionalInfo implement this.setConfiguration(ruleChain.getConfiguration()); this.setExternalId(ruleChain.getExternalId()); this.version = ruleChain.getVersion(); + this.notes = ruleChain.getNotes(); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index c9fdd1a4c6..9109df0f88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.rule; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.HasVersion; @@ -48,6 +49,10 @@ public class RuleChainMetaData implements HasVersion { @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of JSON objects that represent connections between rule nodes and other rule chains.") private List ruleChainConnections; + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "List of sticky notes placed on the rule chain canvas") + private List notes; + public void addConnectionInfo(int fromIndex, int toIndex, String type) { NodeConnectionInfo connectionInfo = new NodeConnectionInfo(); connectionInfo.setFromIndex(fromIndex); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java new file mode 100644 index 0000000000..96715a6700 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.rule; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * Represents a sticky note on the rule chain canvas. + * Notes are purely visual metadata and are stored as part of the rule chain. + */ +@Schema +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RuleChainNote { + + @Schema(description = "Unique identifier of the note on the canvas") + private String id; + + @Schema(description = "Horizontal position of the note on the canvas, in pixels") + private int x; + + @Schema(description = "Vertical position of the note on the canvas, in pixels") + private int y; + + @Schema(description = "Width of the note, in pixels") + private int width; + + @Schema(description = "Height of the note, in pixels") + private int height; + + @Schema(description = "Markdown or HTML content of the note") + private String content; + + @Schema(description = "Background color of the note in CSS hex format, e.g. '#FFF9C4'") + private String backgroundColor; + + @Schema(description = "Whether to apply the default markdown stylesheet to the note content") + private Boolean applyDefaultMarkdownStyle; + + @Schema(description = "Custom CSS styles applied to the note content") + private String markdownCss; + +} 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 9f9e4a67e9..6eb14092ae 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 @@ -420,6 +420,7 @@ public class ModelConstants { public static final String RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY = "first_rule_node_id"; public static final String RULE_CHAIN_ROOT_PROPERTY = "root"; public static final String RULE_CHAIN_CONFIGURATION_PROPERTY = "configuration"; + public static final String RULE_CHAIN_NOTES_PROPERTY = "notes"; /** * Rule node constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java index 5b83eb6f72..e408f41431 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java @@ -69,6 +69,9 @@ public class RuleChainEntity extends BaseVersionedEntity { @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; + @Column(name = ModelConstants.RULE_CHAIN_NOTES_PROPERTY, columnDefinition = "TEXT") + private String notes; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -87,6 +90,7 @@ public class RuleChainEntity extends BaseVersionedEntity { this.debugMode = ruleChain.isDebugMode(); this.configuration = ruleChain.getConfiguration(); this.additionalInfo = ruleChain.getAdditionalInfo(); + this.notes = ruleChain.getNotes(); if (ruleChain.getExternalId() != null) { this.externalId = ruleChain.getExternalId().getId(); } @@ -107,6 +111,7 @@ public class RuleChainEntity extends BaseVersionedEntity { ruleChain.setDebugMode(debugMode); ruleChain.setConfiguration(configuration); ruleChain.setAdditionalInfo(additionalInfo); + ruleChain.setNotes(notes); if (externalId != null) { ruleChain.setExternalId(new RuleChainId(externalId)); } 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 6b90b6fac6..3de45bcc15 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.rule; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FluentFuture; @@ -53,6 +54,7 @@ import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainNote; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; import org.thingsboard.server.common.data.rule.RuleNode; @@ -316,6 +318,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (!relations.isEmpty()) { relationService.saveRelations(tenantId, relations); } + ruleChain.setNotes(ruleChainMetaData.getNotes() != null && !ruleChainMetaData.getNotes().isEmpty() + ? JacksonUtil.toString(ruleChainMetaData.getNotes()) : null); ruleChain = ruleChainDao.save(tenantId, ruleChain); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain) .entityId(ruleChain.getId()).broadcastEvent(publishSaveEvent).build()); @@ -376,6 +380,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC Collections.sort(ruleChainMetaData.getConnections(), Comparator.comparingInt(NodeConnectionInfo::getFromIndex) .thenComparing(NodeConnectionInfo::getToIndex).thenComparing(NodeConnectionInfo::getType)); } + if (ruleChain.getNotes() != null) { + ruleChainMetaData.setNotes(JacksonUtil.fromString(ruleChain.getNotes(), new TypeReference>() {})); + } return ruleChainMetaData; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 3d9c9f17e6..c32ea29d10 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -169,6 +169,7 @@ CREATE TABLE IF NOT EXISTS rule_chain ( debug_mode boolean, tenant_id uuid, external_id uuid, + notes text, version BIGINT DEFAULT 1, CONSTRAINT rule_chain_external_id_unq_key UNIQUE (tenant_id, external_id) ); From 8b2f651644aece764af48419cb34bbc644c82134 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 27 Feb 2026 18:53:57 +0200 Subject: [PATCH 117/313] UI: Fixed context menu for edit note --- .../modules/home/pages/rulechain/rulechain-page.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 8391c874c8..f76fe391ee 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 @@ -1363,8 +1363,8 @@ export class RuleChainPageComponent extends PageComponent const contextInfo: RuleChainMenuContextInfo = { headerClass: 'tb-rulechain-header', icon: 'sticky_note_2', - title: note.content || '', - subtitle: this.translate.instant('rulechain.note'), + title: this.translate.instant('rulechain.note'), + subtitle: note.content.length > 24 ? note.content.substring(0, 24) + '…' : note.content, menuItems: [] }; if (!note.readonly) { From 664aeca9303936c7d607cf20aff47bd36c8cc5ef Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 27 Feb 2026 19:15:11 +0200 Subject: [PATCH 118/313] UI: Fixed rule chain note after review --- .../rulechain/add-note-dialog.component.html | 4 +-- ...t.html => rule-note-editor.component.html} | 0 ...onent.ts => rule-note-editor.component.ts} | 12 ++++----- .../rulechain/rulechain-page.component.html | 4 +-- .../rulechain/rulechain-page.component.ts | 27 +++++++++++-------- .../pages/rulechain/rulechain-page.module.ts | 4 +-- 6 files changed, 28 insertions(+), 23 deletions(-) rename ui-ngx/src/app/modules/home/pages/rulechain/{rule-note.component.html => rule-note-editor.component.html} (100%) rename ui-ngx/src/app/modules/home/pages/rulechain/{rule-note.component.ts => rule-note-editor.component.ts} (82%) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html index 5c98e69cd8..6359e32ff1 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html @@ -23,8 +23,8 @@
- - + +
- + @if (!isImport) { + + + } + @if (lwm2mConfigFormGroup.get('client.securityConfigClientMode').value === securityConfigLwM2MType.PSK) { + + {{ 'device.lwm2m-security-config.identity' | translate }} + help + + @if (lwm2mConfigFormGroup.get('client.identity').hasError('required')) { + {{ 'device.lwm2m-security-config.identity-required' | translate }} + } + + } + @if (lwm2mConfigFormGroup.get('client.securityConfigClientMode').value === securityConfigLwM2MType.RPK || + lwm2mConfigFormGroup.get('client.securityConfigClientMode').value === securityConfigLwM2MType.PSK) { + + {{ 'device.lwm2m-security-config.client-key' | translate }} + help + + @if (lwm2mConfigFormGroup.get('client.key').hasError('required')) { + {{ 'device.lwm2m-security-config.client-key-required' | translate }} + } + + } + @if (lwm2mConfigFormGroup.get('client.securityConfigClientMode').value === securityConfigLwM2MType.X509) { + + device.lwm2m-security-config.client-public-key + @if (lwm2mConfigFormGroup.get('client.cert').value) { + help + } + + device.lwm2m-security-config.client-public-key-hint + + } + @if (deviceId) { + + } @@ -109,11 +118,12 @@
- + @if (deviceId) { + + } diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss index cef379e280..13dd61a4e7 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss @@ -27,4 +27,7 @@ .mat-mdc-tab-body { padding: 16px 0; } + .mat-mdc-tab-body-content { + padding-top: 4px; + } } diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html index f36eaa4565..deaaee3063 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html @@ -16,90 +16,87 @@ -->
- + device.client-id - - - - - - - {{ 'device.client-id-pattern' | translate }} - + + @if (!deviceCredentialsMqttFormGroup.get('clientId').value && !disabled) { + + } @else if (deviceCredentialsMqttFormGroup.get('clientId').value) { + + + } + + @if (deviceCredentialsMqttFormGroup.get('clientId').hasError('pattern')) { + {{ 'device.client-id-pattern' | translate }} + } - + device.user-name - - - - - - - {{ 'device.user-name-required' | translate }} - + + @if (!deviceCredentialsMqttFormGroup.get('userName').value && !disabled) { + + } @else if (deviceCredentialsMqttFormGroup.get('userName').value) { + + + } + + @if (deviceCredentialsMqttFormGroup.get('userName').hasError('required')) { + {{ 'device.user-name-required' | translate }} + } - + device.password - - - - - + + @if (!deviceCredentialsMqttFormGroup.get('password').value && !disabled) { + + } @else if (deviceCredentialsMqttFormGroup.get('password').value) { + + + } + 1)" style="margin-bottom: 16px">
device.credentials-type
- - {{ credentialTypeNamesMap.get(credentialsType) }} - + @for (credentialsType of credentialsTypes; track credentialsType) { + + {{ credentialTypeNamesMap.get(credentialsType) }} + + }
-
- - + @switch (deviceCredentialsFormGroup.get('credentialsType').value) { + @case (deviceCredentialsType.ACCESS_TOKEN) { + device.access-token - - + + @if (!deviceCredentialsFormGroup.get('credentialsId').value && !disabled) { + + } @else if (deviceCredentialsFormGroup.get('credentialsId').value) { + + + } + + @if (deviceCredentialsFormGroup.get('credentialsId').hasError('required')) { + {{ 'device.access-token-required' | translate }} + } + @if (deviceCredentialsFormGroup.get('credentialsId').hasError('pattern')) { + {{ 'device.access-token-invalid' | translate }} + } + + } + @case (deviceCredentialsType.X509_CERTIFICATE) { + + device.certificate-pem-format + + @if (deviceCredentialsFormGroup.get('credentialsValue').value) { - - - {{ 'device.access-token-required' | translate }} - - - {{ 'device.access-token-invalid' | translate }} - - - - - - device.certificate-pem-format - - - - - {{ 'device.certificate-pem-format-required' | translate }} - + } + @if (deviceCredentialsFormGroup.get('credentialsValue').hasError('required')) { + {{ 'device.certificate-pem-format-required' | translate }} + } - - + } + @case (deviceCredentialsType.MQTT_BASIC) { - - + } + @case (deviceCredentialsType.LWM2M_CREDENTIALS) { - -
+ } + } diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index e5b3d57368..de39c93a2f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -25,9 +25,11 @@ close - - -
+@if (isLoading$ | async) { + +} @else { +
+}
{{ 'device-profile.device-profile-details' | translate }} -
- +
+ device-profile.name - - {{ 'device-profile.name-required' | translate }} - - - {{ 'device-profile.name-max-length' | translate }} - + @if (deviceProfileDetailsFormGroup.get('name').hasError('required')) { + {{ 'device-profile.name-required' | translate }} + } + @if (deviceProfileDetailsFormGroup.get('name').hasError('maxlength')) { + {{ 'device-profile.name-max-length' | translate }} + } {{'device-profile.mobile-dashboard-hint' | translate}} {{'device-profile.default-edge-rule-chain-hint' | translate}} - + device-profile.type - - {{deviceProfileTypeTranslations.get(type) | translate}} - + @for (type of deviceProfileTypes; track type) { + + {{ deviceProfileTypeTranslations.get(type) | translate }} + + } - - {{ 'device-profile.type-required' | translate }} - + @if (deviceProfileDetailsFormGroup.get('type').hasError('required')) { + {{ 'device-profile.type-required' | translate }} + } - + device-profile.description - +
@@ -92,24 +100,30 @@
{{ 'device-profile.transport-configuration' | translate }} - - device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} - - - - {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} - - - - +
+ + device-profile.transport-type + + @for (type of deviceTransportTypes; track type) { + + {{ deviceTransportTypeTranslations.get(type) | translate }} + + } + + @if (transportConfigFormGroup.get('transportType').value) { + + {{ deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate }} + + } + @if (transportConfigFormGroup.get('transportType').hasError('required')) { + {{ 'device-profile.transport-type-required' | translate }} + } + + + +
@@ -124,15 +138,18 @@
- + @if (selectedIndex > 0) { + + } - + @if (showNext) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss index 8363e183bb..efc1d43647 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss @@ -52,6 +52,7 @@ width: 100%;; overflow-y: auto; scrollbar-gutter: stable; + padding-top: 4px; @media #{$mat-gt-sm} { min-width: 500px; } diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html index 4f8f3e035b..9a349c3ef4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ 'asset-profile.asset-profile' | translate }} (); diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html index ea859f4b40..9782cf9d19 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html @@ -15,70 +15,76 @@ limitations under the License. --> -
- - - - -
- + + + +
+ +
-
+}
-
- +
+ asset-profile.name - - {{ 'asset-profile.name-required' | translate }} - - - {{ 'asset-profile.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'asset-profile.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'asset-profile.name-max-length' | translate }} + } {{'asset-profile.mobile-dashboard-hint' | translate}} {{'asset-profile.default-edge-rule-chain-hint' | translate}} @@ -87,7 +93,7 @@ label="{{'asset-profile.image' | translate}}" formControlName="image"> - + asset-profile.description diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html index f11bca1242..f73e7a30a1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -16,25 +16,27 @@ -->
- + device-profile.provision-strategy - - {{deviceProvisionTypeTranslateMap.get(type) | translate}} - + @for (type of deviceProvisionTypes; track type) { + + {{ getDeviceProvisionType(type) | translate }} + + } - - {{ 'device-profile.provision-strategy-required' | translate }} - + @if (provisionConfigurationFormGroup.get('type').hasError('required')) { + {{ 'device-profile.provision-strategy-required' | translate }} + } -
- + @switch (provisionConfigurationFormGroup.get('type').value) { + @case (deviceProvisionType.ALLOW_CREATE_NEW_DEVICES) { - - + } + @case (deviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES) { - - + } + @case (deviceProvisionType.X509_CERTIFICATE_CHAIN) {
- + device-profile.provision-strategy-x509.certificate-value - - - {{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }} - + + @if (provisionConfigurationFormGroup.get('certificateValue').hasError('required')) { + {{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }} + } - + device-profile.provision-strategy-x509.cn-regex-variable - - {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} - + tb-help-popup="device-profile/x509-chain-regex-examples" + tb-help-popup-placement="top" + trigger-style="letter-spacing:0.25px" + [tb-help-popup-style]="{maxWidth: '820px'}">
+ @if (provisionConfigurationFormGroup.get('certificateRegExPattern').hasError('required')) { + {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} + } device-profile.provision-strategy-x509.cn-regex-variable-hint - - -
- - device-profile.provision-device-key - - - - {{ 'device-profile.provision-device-key-required' | translate }} - - - - device-profile.provision-device-secret - - - - {{ 'device-profile.provision-device-secret-required' | translate }} - - -
-
-
+ } + }
+ +
+ + device-profile.provision-device-key + + + @if (provisionConfigurationFormGroup.get('provisionDeviceKey').hasError('required')) { + {{ 'device-profile.provision-device-key-required' | translate }} + } + + + device-profile.provision-device-secret + + + @if (provisionConfigurationFormGroup.get('provisionDeviceSecret').hasError('required')) { + {{ 'device-profile.provision-device-secret-required' | translate }} + } + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 1da59bbc5c..4065dc602d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -213,4 +213,8 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup.get('provisionDeviceKey').reset({value: null, disabled: true}, {emitEvent: false}); } } + + getDeviceProvisionType(type: string): string { + return this.deviceProvisionTypeTranslateMap.get(type as DeviceProvisionType); + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index d4519b8e61..9b54e59253 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -15,79 +15,90 @@ limitations under the License. --> -
- - - - -
- + + + +
+ +
-
+}
-
- +
+ device-profile.name - - {{ 'device-profile.name-required' | translate }} - - - {{ 'device-profile.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'device-profile.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'device-profile.name-max-length' | translate }} + } - {{'device-profile.mobile-dashboard-hint' | translate}} + @if (!entityForm.get('defaultDashboardId').disabled) { + {{'device-profile.mobile-dashboard-hint' | translate}} + } - {{'device-profile.default-edge-rule-chain-hint' | translate}} + @if (!entityForm.get('defaultEdgeRuleChainId').disabled) { + {{'device-profile.default-edge-rule-chain-hint' | translate}} + } - + device-profile.type - - {{deviceProfileTypeTranslations.get(type) | translate}} - + @for (type of deviceProfileTypes; track type) { + + {{ deviceProfileTypeTranslations.get(type) | translate }} + + } - - {{ 'device-profile.type-required' | translate }} - + @if (entityForm.get('type').hasError('required')) { + {{ 'device-profile.type-required' | translate }} + } - + device-profile.description - - device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} - - - - {{ 'device-profile.transport-type-required' | translate }} - - -
- - - - -
device-profile.profile-configuration
-
-
- - - - -
- - - -
device-profile.transport-configuration
-
-
- - - - -
- @if (entityForm.get('profileData.alarms').value?.length) { - + @if (standalone) { + + device-profile.transport-type + + @for (type of deviceTransportTypes; track type) { + + {{ deviceTransportTypeTranslations.get(type) | translate }} + + } + + @if (entityForm.get('transportType').hasError('required')) { + {{ 'device-profile.transport-type-required' | translate }} + } + +
+ + @if (displayProfileConfiguration) { + + + +
device-profile.profile-configuration
+
+
+ + + + +
+ } + @if (displayTransportConfiguration) { + + + +
device-profile.transport-configuration
+
+
+ + + + +
+ } + @if (entityForm.get('profileData.alarms').value?.length) { + + + +
{{'device-profile.alarm-rules-with-count' | translate: + {count: entityForm.get('profileData.alarms').value ? + entityForm.get('profileData.alarms').value.length : 0} }}
+
+
+ + + + +
+ } + -
{{'device-profile.alarm-rules-with-count' | translate: - {count: entityForm.get('profileData.alarms').value ? - entityForm.get('profileData.alarms').value.length : 0} }}
+
device-profile.device-provisioning
- - + +
- } - - - -
device-profile.device-provisioning
-
-
- - - - -
-
-
+
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html index 50f1040052..d345fc789c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html @@ -20,81 +20,81 @@
device-profile.coap-device-type
- + - - {{ coapTransportDeviceTypeTranslations.get(type) | translate}} - + @for (type of coapTransportDeviceTypes; track type) { + + {{ coapTransportDeviceTypeTranslations.get(type) | translate }} + + } - - {{ 'device-profile.coap-device-type-required' | translate }} - + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.coapDeviceType').hasError('required')) { + {{ 'device-profile.coap-device-type-required' | translate }} + }
-
-
- device-profile.coap-device-payload-type -
- - - - {{ transportPayloadTypeTranslations.get(type) | translate}} - - - - {{ 'device-profile.mqtt-payload-type-required' | translate }} - - -
- - - - - {{ 'device-profile.telemetry-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.attributes-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.rpc-request-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.rpc-response-proto-schema-required' | translate}} - - + @if (coapDeviceTypeDefault) { +
+
+ device-profile.coap-device-payload-type +
+ + + @for (type of transportPayloadTypes; track type) { + + {{ transportPayloadTypeTranslations.get(type) | translate }} + + } + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.transportPayloadType').hasError('required')) { + {{ 'device-profile.mqtt-payload-type-required' | translate }} + } + + @if (protoPayloadType) { +
+ + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')) { + {{ 'device-profile.telemetry-proto-schema-required' | translate }} + } + + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')) { + {{ 'device-profile.attributes-proto-schema-required' | translate }} + } + + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')) { + {{ 'device-profile.rpc-request-proto-schema-required' | translate }} + } + + + @if (coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')) { + {{ 'device-profile.rpc-response-proto-schema-required' | translate }} + } +
+ }
-
-
-
+
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html b/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html index 85328ee0d1..e96c05fb0c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html @@ -16,37 +16,48 @@ -->
- - {{ "device-profile.power-saving-mode" | translate }} + + @if (isDeviceSetting) { + {{ "device-profile.power-saving-mode" | translate }} + } - {{ "device-profile.power-saving-mode-type.default" | translate }} - - {{ powerModeTranslationMap.get(powerMod) | translate}} - + @if (isDeviceSetting) { + {{ "device-profile.power-saving-mode-type.default" | translate }} + } + @for (powerMod of powerMods; track powerMod) { + + {{ powerModeTranslationMap.get(powerMod) | translate}} + + } -
- - - + + + + +
+ } @else if (parentForm.get('powerMode').value === 'PSM') { + + [minText]="'device-profile.psm-activity-timer-min'" + appearance="outline" + formControlName="psmActivityTimer"> -
- - + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html index 132d46ab69..511ec76673 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html @@ -16,7 +16,7 @@ -->
- + {{ labelText | translate }} @@ -29,7 +29,7 @@ {{ (minText || patternText) | translate : {min: minTime/1000} }} - + device-profile.condition-duration-time-unit diff --git a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts index 709d4ececa..29cf614c2e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts @@ -37,6 +37,7 @@ import { timeUnitTranslationMap } from '@shared/models/time/time.models'; import { isDefinedAndNotNull, isNumber } from '@core/utils'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; interface FormGroupModel { time: number; @@ -105,6 +106,9 @@ export class TimeUnitSelectComponent implements OnInit, OnDestroy, ControlValueA @Input() minText: string; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private propagateChange = (v: any) => { } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html index d738e0447a..1074428d93 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html @@ -17,25 +17,31 @@ -->
-
- - -
+ @for (serverConfig of serverConfigsFromArray.controls; track trackByParams($index)) { +
+ + +
+ }
-
+ @if (!serverConfigsFromArray.controls.length) { +
device-profile.lwm2m.no-config-servers -
-
-
+ } + @if (!disabled) { +
+ -
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts index 2f0fd69102..3b6d676ab3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts @@ -18,11 +18,12 @@ import { Component, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output } import { AbstractControl, ControlValueAccessor, - UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, NG_VALIDATORS, - NG_VALUE_ACCESSOR + NG_VALUE_ACCESSOR, + FormControl, + FormArray } from '@angular/forms'; import { of, Subject } from 'rxjs'; import { ServerSecurityConfig } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; @@ -104,8 +105,8 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu this.destroy$.complete(); } - get serverConfigsFromArray(): UntypedFormArray { - return this.bootstrapConfigServersFormGroup.get('serverConfigs') as UntypedFormArray; + get serverConfigsFromArray(): FormArray { + return this.bootstrapConfigServersFormGroup.get('serverConfigs') as FormArray; } setDisabledState(isDisabled: boolean): void { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html index 3f38a5c33a..e69cb9660c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html @@ -22,160 +22,180 @@
{{ (serverFormGroup.get('bootstrapServerIs').value ? 'device-profile.lwm2m.bootstrap-server' : 'device-profile.lwm2m.lwm2m-server') | translate }}
-
-
{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }} - {{ serverFormGroup.get('shortServerId').value ? serverFormGroup.get('shortServerId').value : '' }} + @if (!serverPanel.expanded) { +
+
{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }} + {{ serverFormGroup.get('shortServerId').value ? serverFormGroup.get('shortServerId').value : '' }} +
+
{{ ('device-profile.lwm2m.mode' | translate) + ': ' }} + {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[serverFormGroup.get('securityMode').value]) }} +
-
{{ ('device-profile.lwm2m.mode' | translate) + ': ' }} - {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[serverFormGroup.get('securityMode').value]) }} -
-
+ }
- + @if (!disabled) { + + }
- + {{ 'device-profile.lwm2m.mode' | translate }} - - {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityMode]) }} - + @for (securityMode of securityConfigLwM2MTypes; track securityMode) { + + {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityMode]) }} + + } - - {{ 'device-profile.lwm2m.short-id' | translate }} - help - - - {{ 'device-profile.lwm2m.short-id-required' | translate }} - - - {{ (isBootstrap ? 'device-profile.lwm2m.short-id-pattern-bs' : 'device-profile.lwm2m.short-id-pattern') | translate }} - - - {{ 'device-profile.lwm2m.short-id-range' | translate : {min: shortServerIdMin, max: shortServerIdMax} }} - - + @if (!isBootstrap) { + + {{ 'device-profile.lwm2m.short-id' | translate }} + @if (!disabled) { + help + } + + @if (serverFormGroup.get('shortServerId').hasError('required')) { + {{ 'device-profile.lwm2m.short-id-required' | translate }} + } + @if (serverFormGroup.get('shortServerId').hasError('pattern')) { + {{ (isBootstrap ? 'device-profile.lwm2m.short-id-pattern-bs' : 'device-profile.lwm2m.short-id-pattern') | translate }} + } + @if (serverFormGroup.get('shortServerId').hasError('min') || + serverFormGroup.get('shortServerId').hasError('max')) { + {{ 'device-profile.lwm2m.short-id-range' | translate : {min: shortServerIdMin, max: shortServerIdMax} }} + } + + }
- + {{ 'device-profile.lwm2m.server-host' | translate }} - - {{ 'device-profile.lwm2m.server-host-required' | translate }} - + @if (serverFormGroup.get('host').hasError('required')) { + {{ 'device-profile.lwm2m.server-host-required' | translate }} + } - + {{ 'device-profile.lwm2m.server-port' | translate }} - - {{ 'device-profile.lwm2m.server-port-required' | translate }} - - - {{ 'device-profile.lwm2m.server-port-pattern' | translate }} - - - {{ 'device-profile.lwm2m.server-port-range' | translate }} - + @if (serverFormGroup.get('port').hasError('required')) { + {{ 'device-profile.lwm2m.server-port-required' | translate }} + } + @if (serverFormGroup.get('port').hasError('pattern')) { + {{ 'device-profile.lwm2m.server-port-pattern' | translate }} + } + @if (serverFormGroup.get('port').hasError('min') || + serverFormGroup.get('port').hasError('max')) { + {{ 'device-profile.lwm2m.server-port-range' | translate }} + }
- + {{ 'device-profile.lwm2m.client-hold-off-time' | translate }} - help + @if (!disabled) { + help + } - - {{ 'device-profile.lwm2m.client-hold-off-time-required' | translate }} - - - {{ 'device-profile.lwm2m.client-hold-off-time-pattern' | translate }} - + @if (serverFormGroup.get('clientHoldOffTime').hasError('required')) { + {{ 'device-profile.lwm2m.client-hold-off-time-required' | translate }} + } + @if (serverFormGroup.get('clientHoldOffTime').hasError('min') || + serverFormGroup.get('clientHoldOffTime').hasError('pattern')) { + {{ 'device-profile.lwm2m.client-hold-off-time-pattern' | translate }} + } - + {{ 'device-profile.lwm2m.account-after-timeout' | translate }} - help + @if (!disabled) { + help + } - - {{ 'device-profile.lwm2m.account-after-timeout-required' | translate }} - - - {{ 'device-profile.lwm2m.account-after-timeout-pattern' | translate }} - + @if (serverFormGroup.get('bootstrapServerAccountTimeout').hasError('required')) { + {{ 'device-profile.lwm2m.account-after-timeout-required' | translate }} + } + @if (serverFormGroup.get('bootstrapServerAccountTimeout').hasError('min') || + serverFormGroup.get('bootstrapServerAccountTimeout').hasError('pattern')) { + {{ 'device-profile.lwm2m.account-after-timeout-pattern' | translate }} + }
-
- + @if (serverFormGroup.get('securityMode').value === securityConfigLwM2MType.RPK || + serverFormGroup.get('securityMode').value === securityConfigLwM2MType.X509) { + {{ 'device-profile.lwm2m.server-public-key' | translate }} - help + @if (!disabled) { + help + } - - {{ 'device-profile.lwm2m.server-public-key-required' | translate }} - + @if (serverFormGroup.get('serverPublicKey').hasError('required')) { + {{ 'device-profile.lwm2m.server-public-key-required' | translate }} + } -
+ }
- + {{ 'device-profile.lwm2m.lifetime' | translate }} - - {{ 'device-profile.lwm2m.lifetime-required' | translate }} - - - {{ 'device-profile.lwm2m.lifetime-pattern' | translate }} - + @if (serverFormGroup.get('lifetime').hasError('required')) { + {{ 'device-profile.lwm2m.lifetime-required' | translate }} + } + @if (serverFormGroup.get('lifetime').hasError('pattern') || + serverFormGroup.get('lifetime').hasError('min')) { + {{ 'device-profile.lwm2m.lifetime-pattern' | translate }} + } - + {{ 'device-profile.lwm2m.default-min-period' | translate }} - help + @if (!disabled) { + help + } - - {{ 'device-profile.lwm2m.default-min-period-required' | translate }} - - - {{ 'device-profile.lwm2m.default-min-period-pattern' | translate }} - + @if (serverFormGroup.get('defaultMinPeriod').hasError('required')) { + {{ 'device-profile.lwm2m.default-min-period-required' | translate }} + } + @if (serverFormGroup.get('defaultMinPeriod').hasError('pattern') || + serverFormGroup.get('defaultMinPeriod').hasError('min')) { + {{ 'device-profile.lwm2m.default-min-period-pattern' | translate }} + }
- + {{ 'device-profile.lwm2m.binding' | translate }} - help + @if (!disabled) { + help + } - - {{ bindingModeTypeNamesMap.get(bindingMode) | translate }} - + @for (bindingMode of bindingModeTypes; track bindingMode) { + + {{ bindingModeTypeNamesMap.get(bindingMode) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index f61ad0bf68..a09f088853 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -19,10 +19,11 @@ -
+
@@ -30,18 +31,20 @@ {{ 'device-profile.lwm2m.init-attr-tel-as-obs-strategy' | translate }} - + device-profile.lwm2m.observe-strategy.observe-strategy {{ observeStrategyMap.get(lwm2mDeviceProfileFormGroup.get('observeStrategy').value)?.name | translate }} - - {{ observeStrategyMap.get(strategy).name | translate }} - - {{ observeStrategyMap.get(strategy).description | translate }} - - + @for (strategy of observeStrategyList; track strategy) { + + {{ observeStrategyMap.get(strategy).name | translate }} + + {{ observeStrategyMap.get(strategy).description | translate }} + + + }
-
- warning - LwM2M transport was run without bootstrap server -
+ @if (!isTransportWasRunWithBootstrap) { +
+ warning + LwM2M transport was run without bootstrap server +
+ } {{ 'device-profile.lwm2m.include-bootstrap-server' | translate }}
- +
@@ -79,7 +85,7 @@
device-profile.lwm2m.fw-update - + {{ 'device-profile.lwm2m.fw-update-strategy' | translate }} {{ 'device-profile.lwm2m.fw-update-strategy-package' | translate }} @@ -87,30 +93,34 @@ {{ 'device-profile.lwm2m.fw-update-strategy-data' | translate }} - - {{ 'device-profile.lwm2m.fw-update-resource' | translate }} - - - {{ 'device-profile.lwm2m.fw-update-resource-required' | translate }} - - + @if (lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateStrategy').value === 2) { + + {{ 'device-profile.lwm2m.fw-update-resource' | translate }} + + @if (lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateResource').hasError('required')) { + {{ 'device-profile.lwm2m.fw-update-resource-required' | translate }} + } + + }
device-profile.lwm2m.sw-update - + {{ 'device-profile.lwm2m.sw-update-strategy' | translate }} {{ 'device-profile.lwm2m.sw-update-strategy-package' | translate }} {{ 'device-profile.lwm2m.sw-update-strategy-package-uri' | translate }} - - {{ 'device-profile.lwm2m.sw-update-resource' | translate }} - - - {{ 'device-profile.lwm2m.sw-update-resource-required' | translate }} - - + @if (lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateStrategy').value === 2) { + + {{ 'device-profile.lwm2m.sw-update-resource' | translate }} + + @if (lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateResource').hasError('required')) { + {{ 'device-profile.lwm2m.sw-update-resource-required' | translate }} + } + + }
@@ -120,11 +130,13 @@
device-profile.lwm2m.default-object-id - + - - {{ objectIDVerTranslationMap.get(objectIDVer) | translate }} - + @for (objectIDVer of objectIDVers; track objectIDVer) { + + {{ objectIDVerTranslationMap.get(objectIDVer) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html index 5bd040695a..1dea3c6a64 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + device-profile.lwm2m.object-list (); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html index f432533614..a0ab434c9a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.html @@ -17,57 +17,58 @@ -->
- - - -
- {{ 'device-profile.lwm2m.instance' | translate }} #{{instances.get('id').value}} -
-
- - -
-
- - -
-
- - -
- - - -
-
- - - - -
+ @for (instances of instancesFormArray.controls; track trackByParams($index, instances.value)) { + + + +
+ {{ 'device-profile.lwm2m.instance' | translate }} #{{ instances.get('id').value }} +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
+ + + + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html index c125f345a3..38c3f2e087 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html @@ -34,41 +34,42 @@ -
-
-
- #{{resourceLwM2M.get('id').value}} {{resourceLwM2M.get('name').value}} +
+ @for (resourceLwM2M of resourcesFormArray.controls; track trackByParams($index, resourceLwM2M.value)) { +
+
+ #{{ resourceLwM2M.get('id').value }} {{ resourceLwM2M.get('name').value }} +
+
+ + +
+
+ + +
+
+ + +
+ + + @if (resourceLwM2M.get('keyName').hasError('required') || + resourceLwM2M.get('keyName').hasError('pattern')) { + {{ 'device-profile.lwm2m.key-name-required' | translate }} + } + + + +
-
- - -
-
- - -
-
- - -
- - - - {{ 'device-profile.lwm2m.key-name-required' | translate }} - - - - - -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html index aa3ea82f64..46051f9cc5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html @@ -17,31 +17,33 @@ -->
- - - - {{ objectLwM2M.get('name').value}} #{{ objectLwM2M.get('keyId').value}} - - - - - - - - - - - + @for (objectLwM2M of modelsFormArray.controls; track trackByParams($index, objectLwM2M.value)) { + + + + {{ objectLwM2M.get('name').value }} #{{ objectLwM2M.get('keyId').value }} + + + + @if (!disabled && objectLwM2M.get('multiple').value) { + + } + + + + + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html index 71996cc67d..9e2c976c52 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -19,156 +19,150 @@ {{ 'device-profile.mqtt-device-topic-filters-spark-plug' | translate }} -
- - + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value) { +
+ + + } -
-
- device-profile.mqtt-device-topic-filters -
-
- - device-profile.telemetry-topic-filter - - - {{ 'device-profile.telemetry-topic-filter-required' | translate}} - - - {{ 'device-profile.not-valid-single-character' | translate}} - - - {{ 'device-profile.not-valid-multi-character' | translate}} - - - - device-profile.attributes-topic-filter - - - {{ 'device-profile.attributes-topic-filter-required' | translate}} - - - {{ 'device-profile.not-valid-single-character' | translate}} - - - {{ 'device-profile.not-valid-multi-character' | translate}} - - - - device-profile.attributes-subscribe-topic-filter - - - {{ 'device-profile.attributes-subscribe-topic-filter-required' | translate}} - - - {{ 'device-profile.not-valid-single-character' | translate}} - - - {{ 'device-profile.not-valid-multi-character' | translate}} - - -
- - {{ 'device-profile.mqtt-device-topic-filters-unique' | translate }} - -
-
-
-
-
-
+@if (!mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value) { +
- device-profile.mqtt-device-payload-type + device-profile.mqtt-device-topic-filters
- - - - {{transportPayloadTypeTranslations.get(type) | translate}} - - - - {{ 'device-profile.mqtt-payload-type-required' | translate }} - - -
- - {{ 'device-profile.mqtt-enable-compatibility-with-json-payload-format' | translate }} - -
-
- - {{ 'device-profile.mqtt-use-json-format-for-default-downlink-topics' | translate }} - -
-
-
-
- - - - - {{ 'device-profile.telemetry-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.attributes-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.rpc-request-proto-schema-required' | translate}} - - - - - - - {{ 'device-profile.rpc-response-proto-schema-required' | translate}} - - +
+ + device-profile.telemetry-topic-filter + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceTelemetryTopic').hasError('required')) { + {{ 'device-profile.telemetry-topic-filter-required' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceTelemetryTopic').hasError('invalidSingleTopicCharacter')) { + {{ 'device-profile.not-valid-single-character' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceTelemetryTopic').hasError('invalidMultiTopicCharacter')) { + {{ 'device-profile.not-valid-multi-character' | translate }} + } + + + device-profile.attributes-topic-filter + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesTopic').hasError('required')) { + {{ 'device-profile.attributes-topic-filter-required' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesTopic').hasError('invalidSingleTopicCharacter')) { + {{ 'device-profile.not-valid-single-character' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesTopic').hasError('invalidMultiTopicCharacter')) { + {{ 'device-profile.not-valid-multi-character' | translate }} + } + + + device-profile.attributes-subscribe-topic-filter + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesSubscribeTopic').hasError('required')) { + {{ 'device-profile.attributes-subscribe-topic-filter-required' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesSubscribeTopic').hasError('invalidSingleTopicCharacter')) { + {{ 'device-profile.not-valid-single-character' | translate }} + } + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('deviceAttributesSubscribeTopic').hasError('invalidMultiTopicCharacter')) { + {{ 'device-profile.not-valid-multi-character' | translate }} + } +
+ @if (mqttDeviceProfileTransportConfigurationFormGroup.hasError('unique')) { + {{ 'device-profile.mqtt-device-topic-filters-unique' | translate }} + } +
+
+
-
-
- - {{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }} - -
-
-
+
+
+ device-profile.mqtt-device-payload-type +
+ + + @for (type of transportPayloadTypes; track type) { + + {{ getTransportPayloadType(type) | translate }} + + } + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.transportPayloadType').hasError('required')) { + {{ 'device-profile.mqtt-payload-type-required' | translate }} + } + + @if (protoPayloadType) { +
+ + {{ 'device-profile.mqtt-enable-compatibility-with-json-payload-format' | translate }} + +
+ @if (compatibilityWithJsonPayloadFormatEnabled) { + + {{ 'device-profile.mqtt-use-json-format-for-default-downlink-topics' | translate }} + +
+ } +
+
+ + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')) { + {{ 'device-profile.telemetry-proto-schema-required' | translate }} + } + + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')) { + {{ 'device-profile.attributes-proto-schema-required' | translate }} + } + + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')) { + {{ 'device-profile.rpc-request-proto-schema-required' | translate }} + } + + + @if (mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')) { + {{ 'device-profile.rpc-response-proto-schema-required' | translate }} + } +
+ } +
+
+
+
+ + {{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }} + +
+
+ +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts index bc997d6960..d1a6d8ace2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -252,4 +252,8 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control } return null; } + + getTransportPayloadType(type: string): string { + return this.transportPayloadTypeTranslations.get(type as TransportPayloadType); + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html index 0d5d03067b..790c13c47e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html @@ -16,57 +16,66 @@ -->
-
-
- - device-profile.snmp.scope - - - {{ snmpSpecTypeTranslationMap.get(snmpSpecType) }} - - - - {{ 'device-profile.snmp.scope-required' | translate }} - - - -
- - device-profile.snmp.querying-frequency - - - {{ 'device-profile.snmp.querying-frequency-required' | translate }} - - - {{ 'device-profile.snmp.querying-frequency-invalid-format' | translate }} - + @for (deviceProfileCommunication of communicationConfigFormArray.controls; track $index) { +
+
+ + device-profile.snmp.scope + + @for (snmpSpecType of snmpSpecTypes; track snmpSpecType) { + + {{ snmpSpecTypeTranslationMap.get(snmpSpecType) }} + + } + + @if (deviceProfileCommunication.get('spec').hasError('required')) { + {{ 'device-profile.snmp.scope-required' | translate }} + } - - -
+ +
+ @if (isShowFrequency(deviceProfileCommunication.get('spec').value)) { + + device-profile.snmp.querying-frequency + + @if (deviceProfileCommunication.get('queryingFrequencyMs').hasError('required')) { + {{ 'device-profile.snmp.querying-frequency-required' | translate }} + } + @if (deviceProfileCommunication.get('queryingFrequencyMs').hasError('pattern') || + deviceProfileCommunication.get('queryingFrequencyMs').hasError('min')) { + {{ 'device-profile.snmp.querying-frequency-invalid-format' | translate }} + } + + } + + +
+
+ @if (!disabled) { + + }
- -
-
+ } + @if (!communicationConfigFormArray.controls.length && !disabled) { +
device-profile.snmp.please-add-communication-config -
-
- -
+
+ } + @if (!disabled && isAddEnabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts index 544fcfdac6..e2a60ad899 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.ts @@ -24,7 +24,7 @@ import { NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator, - Validators + Validators, FormArray, FormGroup } from '@angular/forms'; import { SnmpCommunicationConfig, SnmpSpecType, SnmpSpecTypeTranslationMap } from '@shared/models/device.models'; import { Subject } from 'rxjs'; @@ -79,8 +79,8 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On this.destroy$.complete(); } - get communicationConfigFormArray(): UntypedFormArray { - return this.deviceProfileCommunicationConfig.get('communicationConfig') as UntypedFormArray; + get communicationConfigFormArray(): FormArray { + return this.deviceProfileCommunicationConfig.get('communicationConfig') as FormArray; } registerOnChange(fn: any): void { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.html index 5e558a51dd..1813e6d35e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.html @@ -17,27 +17,27 @@ -->
- + device-profile.snmp.timeout-ms - - {{ 'device-profile.snmp.timeout-ms-required' | translate }} - - - {{ 'device-profile.snmp.timeout-ms-invalid-format' | translate }} - + @if (snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('required')) { + {{ 'device-profile.snmp.timeout-ms-required' | translate }} + } + @if (snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('pattern') || + snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('min')) { + {{ 'device-profile.snmp.timeout-ms-invalid-format' | translate }} + } - + device-profile.snmp.retries - - {{ 'device-profile.snmp.retries-required' | translate }} - - - {{ 'device-profile.snmp.retries-invalid-format' | translate }} - + @if (snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('required')) { + {{ 'device-profile.snmp.retries-required' | translate }} + } + @if (snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('pattern') || + snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('min')) { + {{ 'device-profile.snmp.retries-invalid-format' | translate }} + }
device-profile.snmp.communication-configs
diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html index efc49b9d82..b4c2901852 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html @@ -15,57 +15,67 @@ limitations under the License. --> -
- - - -
- + + +
+ +
-
+}
- + resource.resource-type - - {{ resourceTypesTranslationMap.get(resourceType) | translate }} - + @for (resourceType of resourceTypes ; track resourceType) { + + {{ resourceTypesTranslationMap.get(resourceType) | translate }} + + } - - resource.title - - - {{ 'resource.title-required' | translate }} - - - {{ 'resource.title-max-length' | translate }} - - + @if (entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd) { + + resource.title + + @if (entityForm.get('title').hasError('required')) { + + {{ 'resource.title-required' | translate }} + + } + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'resource.title-max-length' | translate }} + + } + + } @if (isAdd || ((isAdd || isEdit) && entityForm.get('resourceType').value === resourceType.GENERAL)) { } @else {
- + resource.file-name diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html index df765fcec8..19c65c67f2 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ labelText | translate }}
- - -
+ @if (!hideLoadingBar && isLoading$ | async) { + + + } + @if (hideLoadingBar || (isLoading$ | async) === false) { +
+ }
- + admin.repository-url - - admin.repository-url-required - + @if (repositorySettingsForm.get('repositoryUri').hasError('required')) { + + admin.repository-url-required + + } - + admin.default-branch @@ -53,31 +59,37 @@
admin.authentication-settings - + admin.auth-method - - {{repositoryAuthMethodTranslations.get(method) | translate}} - + @for ( method of repositoryAuthMethods; track method) { + + {{repositoryAuthMethodTranslations.get(method) | translate}} + + }
- + common.username - - {{ 'admin.change-password-access-token' | translate }} - - - admin.password-access-token - - - - + @if (showChangePassword) { + + {{ 'admin.change-password-access-token' | translate }} + + } + @if (changePassword || !showChangePassword) { + + admin.password-access-token + + + + + }
- - {{ 'admin.change-passphrase' | translate }} - - - admin.passphrase - - - + @if (showChangePrivateKeyPassword) { + + {{ 'admin.change-passphrase' | translate }} + + } + @if (changePrivateKeyPassword || !showChangePrivateKeyPassword) { + + admin.passphrase + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index ef80df1e9a..c121678b42 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -25,8 +25,11 @@ close - - +@if (isLoading$ | async) { + +} @else { +
+}
- {{ 'device.wizard.device-details' | translate}} -
- + {{ 'device.wizard.device-details' | translate }} +
+ device.name - - {{ 'device.name-required' | translate }} - - - {{ 'device.name-max-length' | translate }} - + @if (deviceWizardFormGroup.get('name').hasError('required')) { + {{ 'device.name-required' | translate }} + } + @if (deviceWizardFormGroup.get('name').hasError('maxlength')) { + {{ 'device.name-max-length' | translate }} + } - + device.label - - {{ 'device.label-max-length' | translate }} - + @if (deviceWizardFormGroup.get('label').hasError('maxlength')) { + {{ 'device.label-max-length' | translate }} + } @@ -68,18 +72,20 @@ {{ 'device.is-gateway' | translate }} - - {{ 'device.overwrite-activity-time' | translate }} - + @if (deviceWizardFormGroup.get('gateway').value) { + + {{ 'device.overwrite-activity-time' | translate }} + + }
- + device.description @@ -100,15 +106,18 @@
- + @if (selectedIndex > 0) { + + } - + @if (showNext) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss index 9f4c873f35..19883b75ff 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss @@ -74,6 +74,7 @@ max-height: 100%; overflow-y: auto; scrollbar-gutter: stable; + padding-top: 4px; } } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html index 6c85aee415..71de635900 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html @@ -48,71 +48,82 @@
- + javascript.javascript-type - - {{ ResourceSubTypeTranslationMap.get(resourceSubType) | translate }} - + @for (resourceSubType of jsResourceSubTypes; track resourceSubType) { + + {{ ResourceSubTypeTranslationMap.get(resourceSubType) | translate }} + + } - + resource.title - - {{ 'resource.title-required' | translate }} - - - {{ 'resource.title-max-length' | translate }} - + @if (entityForm.get('title').hasError('required')) { + + {{ 'resource.title-required' | translate }} + + } + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'resource.title-max-length' | translate }} + + } - - - -
- - - + @if ((isAdd || isEdit) && entityForm.get('resourceSubType').value === ResourceSubType.EXTENSION) { + + + } + @if (entityForm.get('resourceSubType').value === ResourceSubType.MODULE) { + +
+ + @if ((isAdd || isEdit)) { + + + } +
+
+ } + @if (!isAdd && !isEdit && entityForm.get('resourceSubType').value === ResourceSubType.EXTENSION) { +
+ + resource.file-name + +
- -
- - resource.file-name - - -
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts index 0c87d02563..2ede591e36 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts @@ -173,4 +173,6 @@ export class JsResourceComponent extends EntityComponent implements On this.entityForm.get('content').enable({ emitEvent: false }); } } + + protected readonly resourceType = ResourceType; } diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html index b8e8e86d0f..837342ec57 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html @@ -64,7 +64,7 @@
- asset.assignedToCustomer @@ -75,40 +75,43 @@ {{ 'asset.asset-public' | translate }}
-
- +
+ asset.name - - {{ 'asset.name-required' | translate }} - - - {{ 'asset.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'asset.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'asset.name-max-length' | translate }} + } - + asset.label - - {{ 'asset.label-max-length' | translate }} - + @if (entityForm.get('label').hasError('maxlength')) { + {{ 'asset.label-max-length' | translate }} + } - - + @if (isAdd) { + + + }
- + asset.description diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.html b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html index 08fae60029..cb771237d1 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer.component.html +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html @@ -46,13 +46,14 @@ [class.!hidden]="isEdit"> {{'customer.manage-dashboards' | translate }} - + @if (edgesSupportEnabled()) { + + }
- device.assignedToCustomer @@ -94,37 +94,39 @@ {{ 'device.device-public' | translate }}
-
- +
+ device.name - - {{ 'device.name-required' | translate }} - - - {{ 'device.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'device.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'device.name-max-length' | translate }} + } - + device.label - - {{ 'device.label-max-length' | translate }} - + @if (entityForm.get('label').hasError('maxlength')) { + {{ 'device.label-max-length' | translate }} + } {{ 'device.is-gateway' | translate }} - - {{ 'device.overwrite-activity-time' | translate }} - + @if (entityForm.get('additionalInfo.gateway').value) { + + {{ 'device.overwrite-activity-time' | translate }} + + }
- + device.description diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html index 013cd4e52e..25fe5e2c0c 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html @@ -114,30 +114,27 @@
-
- - - - - - -
+ @if (!upgradeAvailable) { + + } @else { + + }
- edge.assignedToCustomer @@ -148,55 +145,58 @@ {{ 'edge.edge-public' | translate }}
-
- +
+ edge.name - - {{ 'edge.name-required' | translate }} - - - {{ 'edge.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'edge.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'edge.name-max-length' | translate }} + }
- - edge.edge-key - - -
- - edge.edge-secret - - -
-
- +
+ edge.label - - {{ 'edge.label-max-length' | translate }} - + @if (entityForm.get('label').hasError('maxlength')) { + {{ 'edge.label-max-length' | translate }} + }
- + edge.description diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html index 87a114761b..170ae4d6bf 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html @@ -64,7 +64,7 @@
- entity-view.assignedToCustomer @@ -75,29 +75,30 @@ {{ 'entity-view.entity-view-public' | translate }}
-
- +
+ entity-view.name - - {{ 'entity-view.name-required' | translate }} - - - {{ 'entity-view.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'entity-view.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'entity-view.name-max-length' | translate }} + } + [entityType]="entityType.ENTITY_VIEW">
- + + formControlName="entityId" + appearance="outline">
@@ -114,21 +115,24 @@ [entityId]="selectedEntityId | async" formControlName="cs" keysText="entity-view.client-attributes-placeholder" - [dataKeyType]="dataKeyType.attribute"> + [dataKeyType]="dataKeyType.attribute" + appearance="outline"> + [dataKeyType]="dataKeyType.attribute" + appearance="outline"> + [dataKeyType]="dataKeyType.attribute" + appearance="outline"> @@ -143,7 +147,8 @@ [entityId]="selectedEntityId | async" formControlName="timeseries" keysText="entity-view.timeseries-placeholder" - [dataKeyType]="dataKeyType.timeseries"> + [dataKeyType]="dataKeyType.timeseries" + appearance="outline"> @@ -152,14 +157,16 @@ dateText="{{ 'entity-view.start-ts' | translate }}" [maxDate]="maxStartTimeMs | async" formControlName="startTimeMs" + appearance="outline" >
- + entity-view.description diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html index 46b3132100..90008ab846 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.html @@ -22,38 +22,38 @@ {{ 'mobile.show-hidden-pages' | translate }}
- - - - + @if (!readonly) { + + + + + }
-
-
-
- {{ 'mobile.custom-page' | translate }} + @for (menuItemControl of visibleMobilePagesControls(); track trackByMenuItem($index, menuItemControl);) { +
+
+ @if (isCustom(menuItemControl)) { +
+ {{ 'mobile.custom-page' | translate }} +
+ } + @if (dragEnabled) { + + } + +
- - - + @if (!$last && !readonly) { + + }
- -
+ }
- + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html index 9824311079..4c5bcf74ed 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html @@ -34,58 +34,67 @@
- - warning - - - warning - - - warning - - - - + @if (mobilePageRowForm.get('label').hasError('required') && mobilePageRowForm.get('label').touched) { + + warning + + } + @if (mobilePageRowForm.get('label').hasError('pattern') && mobilePageRowForm.get('label').touched) { + + warning + + } + @if (mobilePageRowForm.get('label').hasError('maxlength') && mobilePageRowForm.get('label').touched) { + + warning + + } + @if (isCustomMenuItem) { + + + + }
- - - + @if (!disabled) { + + @if (isDefaultMenuItem) { + + } + @if (isCustomMenuItem) { + + } + } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html index 92ba966aee..11b137d6a9 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -25,9 +25,12 @@ close - - -
+@if (isLoading$ | async) { + +} +@if (!(isLoading$ | async)) { +
+}
@@ -41,15 +44,21 @@ mobile.title - - {{ 'mobile.title-required' | translate }} - - - {{ 'mobile.title-cannot-contain-only-spaces' | translate }} - - - {{ 'mobile.title-max-length' | translate }} - + @if (bundlesForms.get('title').hasError('required')) { + + {{ 'mobile.title-required' | translate }} + + } + @if (bundlesForms.get('title').hasError('pattern')) { + + {{ 'mobile.title-cannot-contain-only-spaces' | translate }} + + } + @if (bundlesForms.get('title').hasError('maxlength')) { + + {{ 'mobile.title-max-length' | translate }} + + }
- + @if (selectedIndex > 0) { + + } - - -
+ @if (isLoading$ | async) { + + } @else { +
+ }
- + notification.name - - {{ 'notification.name-required' | translate }} - + @if (targetNotificationForm.get('name').hasError('required')) { + {{ 'notification.name-required' | translate }} + }
- -
-
{{ notificationTargetTypeTranslationMap.get(notificationTargetType) | translate }}
-
-
-
-
- - notification.recipient-type.user-filters - - - {{ notificationTargetConfigTypeInfoMap.get(type).name | translate }} - - - {{ notificationTargetConfigTypeInfoMap.get(targetNotificationForm.get('configuration.usersFilter.type').value).hint | translate }} - - - -
-
- - {{ 'tenant.tenant' | translate }} - {{ 'tenant-profile.tenant-profile' | translate }} - -
- - - - - - - - -
-
- - - - - - - - -
-
-
- - - - {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }} + @for (notificationTargetType of notificationTargetTypes; track notificationTargetType) { + +
+
{{ notificationTargetTypeTranslationMap.get(notificationTargetType) | translate }}
+
-
- - -
-
-
- - {{ "notification.use-old-api" | translate }} - - - open_in_new - + } + + @if (targetNotificationForm.get('configuration.type').value === notificationTargetType.PLATFORM_USERS) { +
+ + notification.recipient-type.user-filters + + @for (type of notificationTargetConfigTypes; track type) { + + {{ notificationTargetConfigTypeInfoMap.get(type).name | translate }} + + } + + {{ notificationTargetConfigTypeInfoMap.get(targetNotificationForm.get('configuration.usersFilter.type').value).hint | translate }} + + @switch (targetNotificationForm.get('configuration.usersFilter.type').value) { + @case (notificationTargetConfigType.TENANT_ADMINISTRATORS) { + @if (isSysAdmin()) { +
+
+ + {{ 'tenant.tenant' | translate }} + {{ 'tenant-profile.tenant-profile' | translate }} + +
+ @if (targetNotificationForm.get('configuration.usersFilter.filterByTenants').value) { + + + } @else { + + + } +
+ } + } + @case (notificationTargetConfigType.USER_LIST) { + + + } + @case (notificationTargetConfigType.CUSTOMER_USERS) { + + + } + } +
+ } + @if (targetNotificationForm.get('configuration.type').value === notificationTargetType.SLACK) { +
+ + + @for (slackChanelType of slackChanelTypes; track slackChanelType) { + + {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }} + + } + + + +
+ } + @if (targetNotificationForm.get('configuration.type').value === notificationTargetType.MICROSOFT_TEAMS) { +
+
+ + {{ "notification.use-old-api" | translate }} + + + open_in_new + +
+ + {{ (targetNotificationForm.get('configuration.useOldApi').value ? + 'notification.webhook-url' : 'notification.workflow-url') | translate }} + + @if (targetNotificationForm.get('configuration.webhookUrl').hasError('required')) { + {{ (targetNotificationForm.get('configuration.useOldApi').value ? 'notification.webhook-url-required' : 'notification.workflow-url-required') | translate }} + } + + + notification.channel-name + + @if (targetNotificationForm.get('configuration.channelName').hasError('required')) { + {{ 'notification.channel-name-required' | translate }} + } +
- - {{ (targetNotificationForm.get('configuration.useOldApi').value ? - 'notification.webhook-url' : 'notification.workflow-url') | translate }} - - - {{ (targetNotificationForm.get('configuration.useOldApi').value ? 'notification.webhook-url-required' : 'notification.workflow-url-required') | translate }} - - - - notification.channel-name - - - {{ 'notification.channel-name-required' | translate }} - - -
- + } + notification.description diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html index 987e87fb0d..77bbf0e4a0 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/escalation-form.component.html @@ -17,25 +17,28 @@ -->
-
notification.first-recipient
- + @if (systemEscalation) { +
notification.first-recipient
+ } @else {
notification.after + [min]="60000" + [max]="604800000" + disabledAdvanced=""> notification.notify
-
+ } -
-
- notification.no-rule -
-
- -
+ @for (escalationControl of escalationsFormArray.controls; track $index; let isLast = $last) { +
+ + + @if ($index === 0) { + + } + @if (!($index === 0) && !disabled) { + + } +
+ } + @if (!escalationsFormArray.controls.length) { +
+ notification.no-rule +
+ } + @if (!disabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html index dda79636f1..cb2643d893 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html @@ -25,9 +25,11 @@ close - - -
+@if (isLoading$ | async) { + +} @else { +
+}
@@ -37,70 +39,76 @@ {{ 'notification.basic-settings' | translate }} - + notification.rule-name - - {{ 'notification.rule-name-required' | translate }} - + @if (ruleNotificationForm.get('name').hasError('required')) { + {{ 'notification.rule-name-required' | translate }} + } {{ 'notification.rule-enable' | translate }} - + notification.trigger.trigger - - {{ triggerTypeTranslationMap.get(trigger) | translate }} - + @for (trigger of triggerTypes; track trigger) { + + {{ triggerTypeTranslationMap.get(trigger) | translate }} + + } - - {{ 'notification.trigger.trigger-required' | translate }} - + @if (ruleNotificationForm.get('triggerType').hasError('required')) { + {{ 'notification.trigger.trigger-required' | translate }} + } -
- - - -
- + @if (ruleNotificationForm.get('triggerType').value !== triggerType.ALARM) { +
+ + + +
+ } @else {
notification.escalation-chain
notification.recipients
-
+ }
notification.stop-escalation-alarm-status-become
- + alarm.alarm-status-list - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + }
@@ -108,559 +116,608 @@ - - {{ 'notification.alarm-trigger-settings' | translate }} -
-
-
- notification.filter - - - - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} - + @if (ruleNotificationForm.get('triggerType').value === triggerType.ALARM) { + + {{ 'notification.alarm-trigger-settings' | translate }} + +
+
+ notification.filter + + + + alarm.alarm-severity-list + + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + } + + +
+ + notification.notify-on + + @for (alarmAction of alarmActions; track alarmAction) { + + {{ alarmActionTranslationMap.get(alarmAction) | translate }} + + } + @if (alarmTemplateForm.get('triggerConfig.notifyOn').hasError('required')) { + {{ 'notification.notify-on-required' | translate }} + } -
- - notification.notify-on - - - {{ alarmActionTranslationMap.get(alarmAction) | translate }} - - - - {{ 'notification.notify-on-required' | translate }} - - -
-
-
-
- - notification.description - - -
-
-
- - - {{ 'notification.device-activity-trigger-settings' | translate }} -
-
-
- - {{ 'device.device' | translate }} - {{ 'device-profile.device-profile' | translate }} - -
- - - - - - - - - - notification.notify-on - - - {{ deviceEventTranslationMap.get(deviceEvent) | translate }} - - - - {{ 'notification.notify-on-required' | translate }} - - -
-
-
-
- - notification.description - - -
-
-
- - - {{ 'notification.entity-action-trigger-settings' | translate }} -
-
- notification.filter - - -
- notification.status - {{ 'notification.created' | translate }} - {{ 'notification.updated' | translate }} - {{ 'notification.deleted' | translate }}
-
-
-
-
- - notification.description - - -
-
-
- - - {{ 'notification.alarm-comment-trigger-settings' | translate }} -
-
-
- notification.filter - - - - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} - - - - - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - + +
+
+ + notification.description + -
-
- - {{ 'notification.notify-only-user-comments' | translate }} - - - {{ 'notification.notify-on-comment-update' | translate }} -
-
- -
-
- - notification.description - - -
-
-
- - {{ 'notification.alarm-assignment-trigger-settings' | translate }} -
-
-
- notification.filter - - + + + } - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} - + @if (ruleNotificationForm.get('triggerType').value === triggerType.DEVICE_ACTIVITY) { + + {{ 'notification.device-activity-trigger-settings' | translate }} +
+
+
+ + {{ 'device.device' | translate }} + {{ 'device-profile.device-profile' | translate }} + +
+ @if (deviceInactivityTemplateForm.get('triggerConfig.filterByDevice').value) { + + + } @else { + + + } + + notification.notify-on + + @for (deviceEvent of deviceEvents; track deviceEvent) { + + {{ deviceEventTranslationMap.get(deviceEvent) | translate }} + + } + @if (deviceInactivityTemplateForm.get('triggerConfig.notifyOn').hasError('required')) { + {{ 'notification.notify-on-required' | translate }} + } - - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - +
+
+
+
+ + notification.description + +
+
+
+ } + + @if (ruleNotificationForm.get('triggerType').value === triggerType.ENTITY_ACTION) { + + {{ 'notification.entity-action-trigger-settings' | translate }} +
+
+ notification.filter + + +
+ notification.status + {{ 'notification.created' | translate }} + {{ 'notification.updated' | translate }} + {{ 'notification.deleted' | translate }} +
- - notification.notify-on - - - {{ alarmAssignmentActionTranslationMap.get(alarmAssignmentAction) | translate }} - - - - {{ 'notification.notify-on-required' | translate }} - - -
- -
-
- - notification.description - - -
-
-
- - {{ 'notification.rule-engine-events-trigger-settings' | translate }} -
-
-
- notification.rule-engine-filter - - - - rulechain.rulechain-events - - - {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} - - + +
+
+ + notification.description + - - {{ 'notification.only-rule-chain-lifecycle-failures' | translate }} - -
-
- notification.rule-node-filter - - {{ 'notification.track-rule-node-events' | translate }} - -
- - rulenode.rule-node-events - - - {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} - +
+ + + } + + @if (ruleNotificationForm.get('triggerType').value === triggerType.ALARM_COMMENT) { + + {{ 'notification.alarm-comment-trigger-settings' | translate }} +
+
+
+ notification.filter + + + + alarm.alarm-severity-list + + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + } - - {{ 'notification.only-rule-node-lifecycle-failures' | translate }} + + alarm.alarm-status-list + + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + } + + +
+
+ + {{ 'notification.notify-only-user-comments' | translate }} + + + {{ 'notification.notify-on-comment-update' | translate }}
-
-
- -
-
- - notification.description - - -
-
-
+
+ +
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.edge-trigger-settings' | translate }} -
-
-
- notification.filter - - - + @if (ruleNotificationForm.get('triggerType').value === triggerType.ALARM_ASSIGNMENT) { + + {{ 'notification.alarm-assignment-trigger-settings' | translate }} + +
+
+ notification.filter + + + + alarm.alarm-severity-list + + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + } + + + + alarm.alarm-status-list + + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + } + + +
+ notification.notify-on - - - {{ edgeConnectionEventTranslationMap.get(edgeEvent) | translate }} - + + @for (alarmAssignmentAction of alarmAssignmentActions; track alarmAssignmentAction) { + + {{ alarmAssignmentActionTranslationMap.get(alarmAssignmentAction) | translate }} + + } + @if (alarmAssignmentTemplateForm.get('triggerConfig.notifyOn').hasError('required')) { + {{ 'notification.notify-on-required' | translate }} + } -
-
-
-
-
- - notification.description - - -
-
-
+ + +
+
+ + notification.description + + +
+
+ + } - - {{ 'notification.edge-trigger-settings' | translate }} -
-
-
- notification.filter - - -
-
-
-
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT) { + + {{ 'notification.rule-engine-events-trigger-settings' | translate }} +
+
+
+ notification.rule-engine-filter + + + + rulechain.rulechain-events + + @for (componentLifecycleEvent of componentLifecycleEvents; track componentLifecycleEvent) { + + {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} + + } + + + + {{ 'notification.only-rule-chain-lifecycle-failures' | translate }} + +
+
+ notification.rule-node-filter + + {{ 'notification.track-rule-node-events' | translate }} + + @if (ruleEngineEventsTemplateForm.get('triggerConfig.trackRuleNodeEvents').value) { +
+ + rulenode.rule-node-events + + @for (componentLifecycleEvent of componentLifecycleEvents; track componentLifecycleEvent) { + + {{ componentLifecycleEventTranslationMap.get(componentLifecycleEvent) | translate }} + + } + + + + {{ 'notification.only-rule-node-lifecycle-failures' | translate }} + +
+ } +
+
+
+
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.entities-limit-trigger-settings' | translate }} -
-
- notification.filter - - -
- notification.threshold -
- - - - - - % + @if (ruleNotificationForm.get('triggerType').value === triggerType.EDGE_CONNECTION) { + + {{ 'notification.edge-trigger-settings' | translate }} + +
+
+ notification.filter + + + + notification.notify-on + + @for (edgeEvent of edgeConnectionEvents; track edgeEvent) { + + {{ edgeConnectionEventTranslationMap.get(edgeEvent) | translate }} + + } + +
+
+ +
+
+ + notification.description + + +
+
+
+ } + + @if (ruleNotificationForm.get('triggerType').value === triggerType.EDGE_COMMUNICATION_FAILURE) { + + {{ 'notification.edge-trigger-settings' | translate }} +
+
+
+ notification.filter + + +
+
+
+
+
+ + notification.description + + +
+
+
+ } + + @if (ruleNotificationForm.get('triggerType').value === triggerType.ENTITIES_LIMIT) { + + {{ 'notification.entities-limit-trigger-settings' | translate }} +
+
+ notification.filter + + +
+ notification.threshold +
+ + + + + + % + +
-
-
- -
-
- - notification.description - - -
-
-
+
+ +
+
+ + notification.description + + +
+
+ + } - - {{ 'notification.api-usage-trigger-settings' | translate }} -
-
- - api-usage.api-features - - - {{ apiFeatureTranslationMap.get(apiFeature) | translate }} - - - {{ 'notification.api-feature-hint' | translate }} - - - notification.notify-on - - - {{ apiUsageStateValueTranslationMap.get(apiUsageStateValue) | translate }} - - - - {{ 'notification.notify-on-required' | translate }} - - -
-
-
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.API_USAGE_LIMIT) { + + {{ 'notification.api-usage-trigger-settings' | translate }} +
+
+ + api-usage.api-features + + @for (apiFeature of apiFeatures; track apiFeature) { + + {{ apiFeatureTranslationMap.get(apiFeature) | translate }} + + } + + {{ 'notification.api-feature-hint' | translate }} + + + notification.notify-on + + @for (apiUsageStateValue of apiUsageStateValues; track apiUsageStateValue) { + + {{ apiUsageStateValueTranslationMap.get(apiUsageStateValue) | translate }} + + } + + @if (apiUsageLimitTemplateForm.get('triggerConfig.notifyOn').hasError('required')) { + {{ 'notification.notify-on-required' | translate }} + } + +
+
+
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.new-platform-version-trigger-settings' | translate }} -
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.NEW_PLATFORM_VERSION) { + + {{ 'notification.new-platform-version-trigger-settings' | translate }} +
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.rate-limits-trigger-settings' | translate }} -
-
- - -
-
-
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.RATE_LIMITS) { + + {{ 'notification.rate-limits-trigger-settings' | translate }} +
+
+ + +
+
+
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.task-processing-failure-trigger-settings' | translate }} -
-
- - notification.description - - -
-
-
+ @if (ruleNotificationForm.get('triggerType').value === triggerType.TASK_PROCESSING_FAILURE) { + + {{ 'notification.task-processing-failure-trigger-settings' | translate }} +
+
+ + notification.description + + +
+
+
+ } - - {{ 'notification.resources-shortage-trigger-settings' | translate }} -
-
- notification.filter -
- notification.cpu-threshold -
- - - - - - % - + @if (ruleNotificationForm.get('triggerType').value === triggerType.RESOURCES_SHORTAGE) { + + {{ 'notification.resources-shortage-trigger-settings' | translate }} + +
+ notification.filter +
+ notification.cpu-threshold +
+ + + + + + % + +
-
-
- notification.ram-threshold -
- - - - - - % - +
+ notification.ram-threshold +
+ + + + + + % + +
-
-
- notification.storage-threshold -
- - - - - - % - +
+ notification.storage-threshold +
+ + + + + + % + +
-
-
-
-
-
- - notification.description - - -
-
-
+
+ +
+
+ + notification.description + + +
+
+ + } +
- + @if (selectedIndex > 0) { + + }
-
- - - -
- - -
+
+ @if (notificationRequestForm.get('useTemplate').value) { + + + + } @else { + +
@@ -101,16 +102,18 @@
-
+ } + + @if (notificationRequestForm.get('targets').value?.length > 1) { +
+ @for (detail of (preview.recipientsCountByTarget | keyvalue); track detail.key) { +
+ {{ detail.value }}{{ detail.key }} +
+ } +
} - - - + + + @for (recipientTitle of preview.recipientsPreview; track recipientTitle) { + + {{ recipientTitle }} + + } + + + @if (preview.processedTemplates.WEB?.enabled) { +
+
+ mdi:bell-badge +
notification.delivery-method.web-preview
+
+
+ +
+
+ } + @if (preview.processedTemplates.MOBILE_APP?.enabled) { +
+
+ mdi:cellphone-text +
notification.delivery-method.mobile-app-preview
+
+
+
{{ preview.processedTemplates.MOBILE_APP.subject }}
+
{{ preview.processedTemplates.MOBILE_APP.body }}
+
+
+ } + @if (preview.processedTemplates.SMS?.enabled) { +
+
+ mdi:message-processing +
notification.delivery-method.sms-preview
+
+
+ {{ preview.processedTemplates.SMS.body }} +
+
+ } + @if (preview.processedTemplates.EMAIL?.enabled) { +
+
+ mdi:email +
notification.delivery-method.email-preview
+
+
+
{{ preview.processedTemplates.EMAIL.subject }}
+ + +
+
+ } + @if (preview.processedTemplates.SLACK?.enabled) { +
+
+ mdi:slack +
notification.delivery-method.slack-preview
+
+
+ {{ preview.processedTemplates.SLACK.body }} +
+
+ } + @if (preview.processedTemplates.MICROSOFT_TEAMS?.enabled) { +
+
+ mdi:microsoft-teams +
notification.delivery-method.microsoft-teams-preview
+
+
+
{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}
+ {{ preview.processedTemplates.MICROSOFT_TEAMS.body }} + @if (preview.processedTemplates.MICROSOFT_TEAMS.button?.enabled) { + + } +
+
+ } + + }
- + @if (selectedIndex > 0) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html index 9bbe1eaa5c..cb7ed554cb 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html @@ -18,7 +18,7 @@
notification.customize-messages
- {{ 'notification.input-fields-support-templatization' | translate}} + {{ 'notification.input-fields-support-templatization' | translate }}
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).name | translate }} - - - - - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - {{'notification.subject-max-length' | translate : - {length: templateConfigurationForm.get('WEB.subject').getError('maxlength').requiredLength} - }} - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - - {{ 'notification.message-max-length' | translate : - {length: templateConfigurationForm.get('WEB.body').getError('maxlength').requiredLength} - }} - - -
-
- - {{ 'icon.icon' | translate }} - -
- - - - + @if (templateConfigurationForm.get('WEB.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).name | translate }} + + + + + notification.subject + + @if (templateConfigurationForm.get('WEB.subject').hasError('required')) { + {{ 'notification.subject-required' | translate }} + } + @if (templateConfigurationForm.get('WEB.subject').hasError('maxlength')) { + {{ 'notification.subject-max-length' | translate : + {length: templateConfigurationForm.get('WEB.subject').getError('maxlength').requiredLength} + }} + } + + + notification.message + + @if (templateConfigurationForm.get('WEB.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + @if (templateConfigurationForm.get('WEB.body').hasError('maxlength')) { + {{ 'notification.message-max-length' | translate : + {length: templateConfigurationForm.get('WEB.body').getError('maxlength').requiredLength} + }} + } + +
+
+ + {{ 'icon.icon' | translate }} + +
+ + + +
-
- - -
-
-
-
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).name | translate }} - - - - - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - {{ 'notification.subject-max-length' | translate : - {length: templateConfigurationForm.get('MOBILE_APP.subject').getError('maxlength').requiredLength} - }} - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - - {{ 'notification.message-max-length' | translate : - {length: templateConfigurationForm.get('MOBILE_APP.body').getError('maxlength').requiredLength} - }} - - -
-
- - {{ 'icon.icon' | translate }} - -
- - - - + + +
+ + +
+ } + @if (templateConfigurationForm.get('MOBILE_APP.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).name | translate }} + + + + + notification.subject + + @if (templateConfigurationForm.get('MOBILE_APP.subject').hasError('required')) { + {{ 'notification.subject-required' | translate }} + } + @if (templateConfigurationForm.get('MOBILE_APP.subject').hasError('maxlength')) { + {{ 'notification.subject-max-length' | translate : + {length: templateConfigurationForm.get('MOBILE_APP.subject').getError('maxlength').requiredLength} + }} + } + + + notification.message + + @if (templateConfigurationForm.get('MOBILE_APP.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + @if (templateConfigurationForm.get('MOBILE_APP.body').hasError('maxlength')) { + {{ 'notification.message-max-length' | translate : + {length: templateConfigurationForm.get('MOBILE_APP.body').getError('maxlength').requiredLength} + }} + } + +
+
+ + {{ 'icon.icon' | translate }} + +
+ + + +
+ +
- - - -
-
-
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).name | translate }} - - - - - notification.message - - - - {{ 'notification.message-required' | translate }} - - - {{ 'notification.message-max-length' | translate : - {length: templateConfigurationForm.get('SMS.body').getError('maxlength').requiredLength} - }} - - - - -
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).name | translate }} - - - - - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - {{'notification.subject-max-length' | translate : - {length: templateConfigurationForm.get('EMAIL.subject').getError('maxlength').requiredLength} - }} - - - notification.message - - - - {{ 'notification.message-required' | translate }} - - - -
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).name | translate }} - - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - - - -
-
- - - - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).icon }} - {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).name | translate }} - - - - - notification.subject - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - -
-
-
notification.theme-color
- + + +
+ } + @if (templateConfigurationForm.get('SMS.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).name | translate }} + + + + + notification.message + + + @if (templateConfigurationForm.get('SMS.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + @if (templateConfigurationForm.get('SMS.body').hasError('maxlength')) { + {{ 'notification.message-max-length' | translate : + {length: templateConfigurationForm.get('SMS.body').getError('maxlength').requiredLength} + }} + } + + + +
+ } + @if (templateConfigurationForm.get('EMAIL.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).name | translate }} + + + + + notification.subject + + @if (templateConfigurationForm.get('EMAIL.subject').hasError('required')) { + {{ 'notification.subject-required' | translate }} + } + @if (templateConfigurationForm.get('EMAIL.subject').hasError('maxlength')) { + {{ 'notification.subject-max-length' | translate : + {length: templateConfigurationForm.get('EMAIL.subject').getError('maxlength').requiredLength} + }} + } + + notification.message + + + @if ((interacted || templateConfigurationForm.get('EMAIL.body').touched) && templateConfigurationForm.get('EMAIL.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + + +
+ } + @if (templateConfigurationForm.get('SLACK.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).name | translate }} + + + + + notification.message + + @if (templateConfigurationForm.get('SLACK.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + + + +
+ } + @if (templateConfigurationForm.get('MICROSOFT_TEAMS.enabled').value) { +
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).name | translate }} + + + + + notification.subject + + + + notification.message + + @if (templateConfigurationForm.get('MICROSOFT_TEAMS.body').hasError('required')) { + {{ 'notification.message-required' | translate }} + } + +
+
+
notification.theme-color
+ +
+ +
- - - -
-
-
+
+ + + } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html index 7298fb6ed9..dbe49a6238 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -25,8 +25,11 @@ close - - +@if (isLoading$ | async) { + +} @else { +
+}
{{ 'notification.setup' | translate }}
- + notification.name - - {{ 'notification.name-required' | translate }} - - - - notification.type - - - {{ notificationTemplateTypeTranslateMap.get(notificationType).name | translate }} - - + @if (templateNotificationForm.get('name').hasError('required')) { + {{ 'notification.name-required' | translate }} + } + @if (!hideSelectType) { + + notification.type + + @for (notificationType of notificationTypes; track notificationType) { + + {{ notificationTemplateTypeTranslateMap.get(notificationType).name | translate }} + + } + + + }
notification.at-least-one-should-be-selected
-
- - {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} - -
+ @for (deliveryMethods of notificationDeliveryMethods; track deliveryMethods) { +
+ + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} + +
+ }
@@ -86,8 +93,10 @@
- + @if (selectedIndex > 0) { + + } + @if (isSingleton()) { + + }
- - + @if (isAddQueue()) { + + + }
- + rulenode.rule-node-description diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html index 02afcfe886..fbc7e9f286 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html @@ -77,22 +77,22 @@
-
- +
+ rulechain.name - - {{ 'rulechain.name-required' | translate }} - - - {{ 'rulechain.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + {{ 'rulechain.name-required' | translate }} + } + @if (entityForm.get('name').hasError('maxlength')) { + {{ 'rulechain.name-max-length' | translate }} + } {{ 'rulechain.debug-mode' | translate }}
- + rulechain.description diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html index b4721ea309..b3f4d8024f 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html @@ -44,15 +44,19 @@
- + widget.title - - {{ 'widget.title-required' | translate }} - - - {{ 'widget.title-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + + {{ 'widget.title-required' | translate }} + + } + @if (entityForm.get('name').hasError('maxlength')) { + + {{ 'widget.title-max-length' | translate }} + + }
@@ -70,7 +75,7 @@ {{ 'widget.deprecated' | translate }}
- + widget.description {{descriptionInput.value?.length || 0}}/1024 diff --git a/ui-ngx/src/app/shared/components/contact.component.html b/ui-ngx/src/app/shared/components/contact.component.html index 2ae047d443..283ba88061 100644 --- a/ui-ngx/src/app/shared/components/contact.component.html +++ b/ui-ngx/src/app/shared/components/contact.component.html @@ -17,54 +17,56 @@ -->
- + contact.city - - {{ 'contact.city-max-length' | translate }} - + @if (parentForm.get('city').hasError('maxlength')) { + {{ 'contact.city-max-length' | translate }} + } - + contact.state - - {{ 'contact.state-max-length' | translate }} - + @if (parentForm.get('state').hasError('maxlength')) { + {{ 'contact.state-max-length' | translate }} + } - + contact.postal-code - - {{ 'contact.postal-code-invalid' | translate }} - + @if (parentForm.get('zip').hasError('pattern')) { + {{ 'contact.postal-code-invalid' | translate }} + }
- + contact.address - + contact.address2 - + contact.email - - {{ 'user.invalid-email-format' | translate }} - + @if (parentForm.get('email').hasError('email')) { + {{ 'user.invalid-email-format' | translate }} + }
diff --git a/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html b/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html index 7b8c5f81e8..f73f0403b6 100644 --- a/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-state-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ label }} - + - + notification.template - + {{ placeholderText | translate }} + class="mat-block autocomplete-queue" [subscriptSizing]="subscriptSizing" [appearance]="appearance"> {{ 'queue.queue-name' | translate }} >>; diff --git a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html index fe67e82143..3db3d06147 100644 --- a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ labelText }} >; From fdba60ae206fbf969e5bd9036b1a7b8eeacfc471 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 12 Mar 2026 16:03:09 +0200 Subject: [PATCH 152/313] fixed ShortCustomerInfo openapi schema, refactoring --- .../server/common/data/FeaturesInfo.java | 26 +++++++++++++++---- .../server/common/data/ShortCustomerInfo.java | 11 ++++++-- .../server/common/data/SystemInfo.java | 2 ++ .../data/query/RelationsQueryFilter.java | 4 ++- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java index 49d0228256..92a446fcb6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java @@ -15,13 +15,29 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@JsonPropertyOrder({ + "emailEnabled", + "smsEnabled", + "notificationEnabled", + "oauthEnabled", + "twoFaEnabled" +}) +@Schema @Data public class FeaturesInfo { - boolean emailEnabled; - boolean smsEnabled; - boolean notificationEnabled; - boolean oauthEnabled; - boolean twoFaEnabled; + @JsonProperty("emailEnabled") + boolean isEmailEnabled; + @JsonProperty("smsEnabled") + boolean isSmsEnabled; + @JsonProperty("notificationEnabled") + boolean isNotificationEnabled; + @JsonProperty("oauthEnabled") + boolean isOauthEnabled; + @JsonProperty("twoFaEnabled") + boolean isTwoFaEnabled; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java index 930e4dddca..dbed1026a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @@ -39,9 +40,15 @@ public class ShortCustomerInfo { @NoXss private String title; + @JsonProperty("isPublic") @Schema(description = "Indicates special 'Public' customer used to embed dashboards on public websites.") - @Getter @Setter - private boolean isPublic; + private boolean publicCustomer; + + @JsonProperty("isPublic") + public boolean isPublic() { return publicCustomer; } + + @JsonProperty("isPublic") + public void setPublic(boolean publicCustomer) { this.publicCustomer = publicCustomer; } @Override public boolean equals(Object o) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java index 73bd3cddc4..3fd86d1163 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -23,6 +24,7 @@ import java.util.List; @Data public class SystemInfo { @Schema(description = "Is monolith.") + @JsonProperty("monolith") private boolean isMonolith; @Schema(description = "System data.") private List systemData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java index 488d03df63..027c4a7c5c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java @@ -15,16 +15,17 @@ */ package org.thingsboard.server.common.data.query; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.List; import java.util.Set; +@Schema @Data public class RelationsQueryFilter implements EntityFilter { @@ -34,6 +35,7 @@ public class RelationsQueryFilter implements EntityFilter { } private AliasEntityId rootEntity; + @JsonProperty("multiRoot") private boolean isMultiRoot; private EntityType multiRootEntitiesType; private Set multiRootEntityIds; From ba5840ebd7bf11ce24bc9b21157c9525846e3e4a Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 13 Mar 2026 11:35:05 +0200 Subject: [PATCH 153/313] Add custom translate support for gauge widget titles and label widget Apply CustomTranslatePipe to unitTitle and title properties in analogue and digital gauge widgets, and add customTranslation to the label widget controller script to support i18n translations. --- .../src/main/data/json/system/widget_types/label_widget.json | 2 +- .../home/components/widget/lib/analogue-gauge.models.ts | 3 ++- .../app/modules/home/components/widget/lib/digital-gauge.ts | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/label_widget.json b/application/src/main/data/json/system/widget_types/label_widget.json index b8bfc94f41..46d448ac1a 100644 --- a/application/src/main/data/json/system/widget_types/label_widget.json +++ b/application/src/main/data/json/system/widget_types/label_widget.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n \n var imageUrl = self.ctx.settings.backgroundImageUrl ? self.ctx.settings.backgroundImageUrl :\n 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';\n\n function processLabelPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = self.ctx.settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n self.ctx.labels = [];\n\n for (var l = 0; l < configuredLabels.length; l++) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : '${#0}';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : 'rgba(0,0,0,0)';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || 'Roboto';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : 'normal';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : '500';\n localConfig.font.color = settingsFont.color ? settingsFont.color : '#fff';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, self.ctx.data);\n \n var label = {};\n var labelElement = $('
');\n labelElement.css('position', 'absolute');\n labelElement.css('display', 'none');\n labelElement.css('top', '0');\n labelElement.css('left', '0');\n labelElement.css('backgroundColor', localConfig.backgroundColor);\n labelElement.css('color', localConfig.font.color);\n labelElement.css('fontFamily', localConfig.font.family);\n labelElement.css('fontStyle', localConfig.font.style);\n labelElement.css('fontWeight', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n self.ctx.$container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n label.htmlSet = false;\n label.visible = false;\n self.ctx.labels.push(label);\n }\n \n self.ctx.imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true}).subscribe(\n function (transformedUrl) {\n self.ctx.$container.css('background', 'url(\"'+transformedUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\n var bgImg = $('');\n bgImg.hide();\n bgImg.bind('load', function()\n {\n self.ctx.bImageHeight = $(this).height();\n self.ctx.bImageWidth = $(this).width();\n self.onResize();\n });\n self.ctx.$container.append(bgImg);\n bgImg.attr('src', transformedUrl);\n }\n );\n\n self.onDataUpdated();\n}\n\nself.onDataUpdated = function() {\n updateLabels();\n}\n\nself.onResize = function() {\n if (self.ctx.bImageHeight && self.ctx.bImageWidth) {\n var backgroundRect = {};\n var imageRatio = self.ctx.bImageWidth / self.ctx.bImageHeight;\n var componentRatio = self.ctx.width / self.ctx.height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = self.ctx.height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css('top', labelTop + '%');\n label.element.css('left', labelLeft + '%');\n label.element.css('fontSize', fontSize + 'px');\n if (!label.visible) {\n label.element.css('display', 'block');\n label.visible = true;\n }\n }\n } \n}\n\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateLabels() {\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n var updated = false;\n for (var v = 0; v < replaceInfo.variables.length; v++) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n updated = true;\n } else {\n txtVal = val;\n updated = true;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !label.htmlSet) {\n label.element.html(text);\n if (!label.htmlSet) {\n label.htmlSet = true;\n }\n }\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true\n };\n};\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n \n var imageUrl = self.ctx.settings.backgroundImageUrl ? self.ctx.settings.backgroundImageUrl :\n 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';\n\n function processLabelPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = self.ctx.settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n self.ctx.labels = [];\n\n for (var l = 0; l < configuredLabels.length; l++) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : '${#0}';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : 'rgba(0,0,0,0)';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || 'Roboto';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : 'normal';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : '500';\n localConfig.font.color = settingsFont.color ? settingsFont.color : '#fff';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, self.ctx.data);\n \n var label = {};\n var labelElement = $('
');\n labelElement.css('position', 'absolute');\n labelElement.css('display', 'none');\n labelElement.css('top', '0');\n labelElement.css('left', '0');\n labelElement.css('backgroundColor', localConfig.backgroundColor);\n labelElement.css('color', localConfig.font.color);\n labelElement.css('fontFamily', localConfig.font.family);\n labelElement.css('fontStyle', localConfig.font.style);\n labelElement.css('fontWeight', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n self.ctx.$container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n label.htmlSet = false;\n label.visible = false;\n self.ctx.labels.push(label);\n }\n \n self.ctx.imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true}).subscribe(\n function (transformedUrl) {\n self.ctx.$container.css('background', 'url(\"'+transformedUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\n var bgImg = $('');\n bgImg.hide();\n bgImg.bind('load', function()\n {\n self.ctx.bImageHeight = $(this).height();\n self.ctx.bImageWidth = $(this).width();\n self.onResize();\n });\n self.ctx.$container.append(bgImg);\n bgImg.attr('src', transformedUrl);\n }\n );\n\n self.onDataUpdated();\n}\n\nself.onDataUpdated = function() {\n updateLabels();\n}\n\nself.onResize = function() {\n if (self.ctx.bImageHeight && self.ctx.bImageWidth) {\n var backgroundRect = {};\n var imageRatio = self.ctx.bImageWidth / self.ctx.bImageHeight;\n var componentRatio = self.ctx.width / self.ctx.height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = self.ctx.height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css('top', labelTop + '%');\n label.element.css('left', labelLeft + '%');\n label.element.css('fontSize', fontSize + 'px');\n if (!label.visible) {\n label.element.css('display', 'block');\n label.visible = true;\n }\n }\n } \n}\n\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateLabels() {\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n var updated = false;\n for (var v = 0; v < replaceInfo.variables.length; v++) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n updated = true;\n } else {\n txtVal = val;\n updated = true;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n text = self.ctx.utilsService.customTranslation(text);\n if (updated || !label.htmlSet) {\n label.element.html(text);\n if (!label.htmlSet) {\n label.htmlSet = true;\n }\n }\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true\n };\n};\n\nself.onDestroy = function() {\n}\n", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-label-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"backgroundImageUrl\":\"tb-image;/api/images/system/here_map_system_widget_map_image.svg\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"Roboto\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts index a4e220888b..f77a854a77 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts @@ -26,6 +26,7 @@ import { TbUnit } from '@shared/models/unit.models'; import { ValueFormatProcessor } from '@shared/models/widget-settings.models'; import { UnitService } from '@core/services/unit.service'; import { DataKey } from '@shared/models/widget.models'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; export type AnimationRule = 'linear' | 'quad' | 'quint' | 'cycle' | 'bounce' | 'elastic' | 'dequad' | 'dequint' @@ -201,7 +202,7 @@ export abstract class TbAnalogueGauge 0 ? + this.ctx.$injector.get(CustomTranslatePipe).transform(settings.unitTitle && settings.unitTitle.length > 0 ? settings.unitTitle : dataKey.label) : ''), borders: settings.showBorder !== false, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index 75eb1e403d..6d9bef5f2d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -34,6 +34,7 @@ import { } from '@shared/models/widget-settings.models'; import { UnitService } from '@core/services/unit.service'; import { isNotEmptyTbUnits } from '@shared/models/unit.models'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import GenericOptions = CanvasGauges.GenericOptions; // @dynamic @@ -55,7 +56,7 @@ export class TbCanvasDigitalGauge { const keyColor = settings.defaultColor || dataKey.color; this.localSettings.unitTitle = ((settings.showUnitTitle === true) ? - (settings.unitTitle && settings.unitTitle.length > 0 ? + ctx.$injector.get(CustomTranslatePipe).transform(settings.unitTitle && settings.unitTitle.length > 0 ? settings.unitTitle : dataKey.label) : ''); this.localSettings.showUnitTitle = settings.showUnitTitle === true; @@ -87,7 +88,7 @@ export class TbCanvasDigitalGauge { -TbCanvasDigitalGauge.toRadians(settings.donutStartAngle) : null; this.localSettings.title = ((settings.showTitle === true) ? - (settings.title && settings.title.length > 0 ? + ctx.$injector.get(CustomTranslatePipe).transform(settings.title && settings.title.length > 0 ? settings.title : dataKey.label) : ''); if (!this.localSettings.unitTitle && this.localSettings.showTimestamp) { From 2a3c668ec93b025a0f8d90f275b9fde229e3fb74 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 13 Mar 2026 11:55:25 +0200 Subject: [PATCH 154/313] fixed readOnly for entityId, deterministic order of tags, fixed DebugEventFilter field duplication (isError, error), fixed OtaPackage data format --- .../server/config/SwaggerConfiguration.java | 5 ++++- .../server/controller/ApiKeyController.java | 2 +- .../server/controller/DeviceController.java | 22 +++++++++---------- .../EntitiesVersionControlController.java | 6 ++--- .../server/common/data/Dashboard.java | 2 +- .../server/common/data/OtaPackage.java | 2 +- .../common/data/event/DebugEventFilter.java | 8 +++++++ 7 files changed, 29 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 9be6a45464..5098009d3c 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -586,6 +586,9 @@ public class SwaggerConfiguration { if (prop.getDescription() != null) { refSchema.setDescription(prop.getDescription()); } + if (prop.getReadOnly() != null) { + refSchema.setReadOnly(prop.getReadOnly()); + } schema.getProperties().put(propName, refSchema); log.debug("Replaced oneOf with $ref to {} in property {}", baseType, propName); } @@ -596,7 +599,7 @@ public class SwaggerConfiguration { private String tagItemFromPathItem(PathItem item) { var operations = item.readOperationsMap().values(); - var operation = operations.stream().findAny(); + var operation = operations.stream().findFirst(); if (operation.isPresent()) { var tags = operation.get().getTags(); if (tags != null && !tags.isEmpty()) { diff --git a/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java b/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java index 09972bd6c2..ae3ea1c60d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java @@ -68,7 +68,7 @@ public class ApiKeyController extends BaseController { private final ApiKeyService apiKeyService; @ApiOperation(value = "Save API key for user (saveApiKey)", - notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey '." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey {value}'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping(value = "/apiKey") public ApiKey saveApiKey( diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 8e5aa706c3..2f43c39f76 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -206,16 +206,16 @@ public class DeviceController extends BaseController { notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK + "Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" + "You may find the example of device with different type of credentials below: \n\n" + - "- Credentials type: \"Access token\" with device profile ID below: \n\n" + + "- Credentials type: **\"Access token\"** with **device profile ID** below: \n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"Access token\" with device profile default below: \n\n" + + "- Credentials type: **\"Access token\"** with **device profile default** below: \n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"X509\" with device profile ID below: \n\n" + - "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + + "- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" + + "Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + + "- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + - "- You may find the example of LwM2M device and RPK credentials below: \n\n" + + "- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " + @@ -322,14 +322,14 @@ public class DeviceController extends BaseController { "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device.\n" + "The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" + "You may find the example of device with different type of credentials below: \n\n" + - "- Credentials type: \"Access token\" with device ID and with device ID below: \n\n" + + "- Credentials type: **\"Access token\"** with **device ID** and with **device ID** below: \n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"X509\" with device profile ID below: \n\n" + - "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + + "- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" + + "Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + + "- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + - "- You may find the example of LwM2M device and RPK credentials below: \n\n" + + "- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + "Update to real value:\n" + diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java index cf632531ce..8dd607bb6f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java @@ -214,19 +214,19 @@ public class EntitiesVersionControlController extends BaseController { " \"timestamp\": 1655198593000,\n" + " \"id\": \"fd82625bdd7d6131cf8027b44ee967012ecaf990\",\n" + " \"name\": \"Devices and assets - v2.0\",\n" + - " \"author\": \"John Doe \"\n" + + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" + " },\n" + " {\n" + " \"timestamp\": 1655198528000,\n" + " \"id\": \"682adcffa9c8a2f863af6f00c4850323acbd4219\",\n" + " \"name\": \"Update my device\",\n" + - " \"author\": \"John Doe \"\n" + + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" + " },\n" + " {\n" + " \"timestamp\": 1655198280000,\n" + " \"id\": \"d2a6087c2b30e18cc55e7cdda345a8d0dfb959a4\",\n" + " \"name\": \"Devices and assets - v1.0\",\n" + - " \"author\": \"John Doe \"\n" + + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" + " }\n" + " ],\n" + " \"totalPages\": 1,\n" + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java index e9e1658731..2c65a3dfc8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java @@ -33,7 +33,7 @@ import java.util.Optional; import java.util.stream.Collectors; @EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"title", "image", "mobileHide", "mobileOrder", "configuration", "name", "resources"}) +@JsonPropertyOrder({"id", "createdTime", "tenantId", "title", "name", "image", "mobileHide", "mobileOrder", "assignedCustomers", "configuration", "resources", "version"}) public class Dashboard extends DashboardInfo implements ExportableEntity { private static final long serialVersionUID = 872682138346187503L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java index e9f0871999..360d16fbbe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java @@ -31,7 +31,7 @@ public class OtaPackage extends OtaPackageInfo { @Serial private static final long serialVersionUID = 3091601761339422546L; - @Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY, type = "string", format = "byte") private transient ByteBuffer data; public OtaPackage() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java index 8779416106..01463dade8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.event; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.StringUtils; @@ -26,10 +27,17 @@ public abstract class DebugEventFilter implements EventFilter { @Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") protected String server; @Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"}) + @JsonProperty("isError") protected boolean isError; @Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") protected String errorStr; + @JsonProperty("isError") + public boolean isError() { + return isError; + } + + @JsonProperty("isError") public void setIsError(boolean isError) { this.isError = isError; } From 31d6b2094474c6e5e1c60fe9fcc09b23de1a785a Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 13 Mar 2026 12:48:58 +0200 Subject: [PATCH 155/313] fixed ByteBuffer fields for openapi schema --- .../org/thingsboard/server/config/SwaggerConfiguration.java | 4 ++++ .../java/org/thingsboard/server/common/data/OtaPackage.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 5098009d3c..db862e2a43 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -48,6 +48,7 @@ import lombok.extern.slf4j.Slf4j; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.customizers.OperationCustomizer; import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer; +import org.springdoc.core.utils.SpringDocUtils; import org.springdoc.core.models.GroupedOpenApi; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.properties.SwaggerUiConfigProperties; @@ -69,6 +70,7 @@ import org.thingsboard.server.service.security.auth.rest.LoginResponse; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -94,6 +96,8 @@ public class SwaggerConfiguration { @PostConstruct public void configureModelResolver() { ModelResolver.enumsAsRef = true; + SpringDocUtils.getConfig().replaceWithSchema(ByteBuffer.class, + new Schema().type("string").format("byte")); } public static final String LOGIN_ENDPOINT = "/api/auth/login"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java index 360d16fbbe..e9f0871999 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java @@ -31,7 +31,7 @@ public class OtaPackage extends OtaPackageInfo { @Serial private static final long serialVersionUID = 3091601761339422546L; - @Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY, type = "string", format = "byte") + @Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY) private transient ByteBuffer data; public OtaPackage() { From ea3271af3a748e31ec6c5f14d1fa36f6e512c429 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 13 Mar 2026 13:18:06 +0200 Subject: [PATCH 156/313] fixed order for Alarm --- .../java/org/thingsboard/server/common/data/alarm/Alarm.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index abd1515ff2..9f9bdec49d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -84,6 +84,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha @Schema(description = "Timestamp of the alarm assignment, in milliseconds", example = "1634115928465") private long assignTs; @Schema(description = "JSON object with alarm details") + @JsonProperty private transient JsonNode details; @Schema(description = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") private boolean propagate; From c3b0ce2ca27a02e60b51a504f8012fc88daef51c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 13 Mar 2026 16:33:35 +0200 Subject: [PATCH 157/313] fixed openapi schema ordering --- .../server/config/SwaggerConfiguration.java | 136 ++++++++---------- .../controller/TwoFactorAuthController.java | 2 + .../common/data/widget/WidgetTypeDetails.java | 2 +- 3 files changed, 62 insertions(+), 78 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index db862e2a43..dee30e5e09 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -83,6 +83,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -110,6 +111,11 @@ public class SwaggerConfiguration { private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false); private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true); + // Populated by mapAwareConverter, consumed by customOpenApiCustomizer. + // Keyed by Class.getSimpleName() which matches swagger-core's default schema naming. + private final Map> schemaPropertyOrders = new ConcurrentHashMap<>(); + private final Map> schemaOwnProps = new ConcurrentHashMap<>(); + @Value("${swagger.api_path:/api/**}") private String apiPath; @Value("${swagger.security_path_regex}") @@ -312,62 +318,20 @@ public class SwaggerConfiguration { schema.setProperties(null); } } - } else if (schema != null) { - boolean hasAllOf = schema.getAllOf() != null; - boolean hasProps = schema.getProperties() != null && !schema.getProperties().isEmpty(); - if (hasAllOf || hasProps) { - try { - var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType); - var orderedNames = resolvePropertyOrder(cls, beanDesc); - // Reorder top-level properties if present. - // When orderedNames is empty (e.g. for interfaces where Jackson - // returns no properties from beanDesc), fall through to the - // TreeMap fallback which sorts remaining properties alphabetically. - if (hasProps) { - @SuppressWarnings("unchecked") - Map current = schema.getProperties(); - var reordered = new LinkedHashMap(); - for (String name : orderedNames) { - Schema prop = current.get(name); - if (prop != null) reordered.put(name, prop); - } - // Any properties not covered by orderedNames are appended - // alphabetically to guarantee a deterministic stable order. - new TreeMap<>(current).forEach((k, v) -> reordered.putIfAbsent(k, v)); - schema.setProperties(reordered); - } - // Also reorder properties inside allOf inline elements, and mark - // which properties are declared in cls itself (not inherited from - // a superclass) so deduplicateAllOfProperties can strip inherited - // ones without touching own-class properties. - if (hasAllOf) { - Set ownProps = computeOwnPropNames(cls, beanDesc); - if (!ownProps.isEmpty()) { - schema.addExtension("x-tb-own-props", List.copyOf(ownProps)); - } - @SuppressWarnings("unchecked") - List allOfList = schema.getAllOf(); - for (Schema allOfElement : allOfList) { - if (allOfElement.get$ref() != null) continue; - @SuppressWarnings("unchecked") - Map inlineProps = allOfElement.getProperties(); - if (inlineProps == null || inlineProps.isEmpty()) continue; - var reordered = new LinkedHashMap(); - for (String name : orderedNames) { - Schema prop = inlineProps.get(name); - if (prop != null) reordered.put(name, prop); - } - new TreeMap<>(inlineProps).forEach((k, v) -> reordered.putIfAbsent(k, v)); - allOfElement.setProperties(reordered); - } - } - } catch (Exception e) { - log.debug("Failed to resolve property order for {}: {}", cls.getName(), e.getMessage()); - // Fallback: at minimum sort alphabetically for determinism - if (hasProps) { - schema.setProperties(new LinkedHashMap<>(new TreeMap<>(schema.getProperties()))); - } + } else { + // Precompute property order and own-prop names for this class. + // The actual reordering happens later in the OpenApiCustomizer, + // which has access to the final state of all component schemas + // (including ones where the ModelConverter only sees a $ref). + try { + var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType); + schemaPropertyOrders.put(cls.getSimpleName(), resolvePropertyOrder(cls, beanDesc)); + Set ownProps = computeOwnPropNames(cls, beanDesc); + if (!ownProps.isEmpty()) { + schemaOwnProps.put(cls.getSimpleName(), ownProps); } + } catch (Exception e) { + log.debug("Failed to resolve property order for {}: {}", cls.getName(), e.getMessage()); } } } @@ -454,16 +418,18 @@ public class SwaggerConfiguration { }); // Deduplicate allOf child schemas: remove properties that are already defined - // in the referenced parent schema to avoid duplication (e.g. EntityId children), - // then clean up the internal marker extension used during deduplication. - schemas.values().forEach(schema -> { - deduplicateAllOfProperties(schema, schemas); - if (schema.getExtensions() != null) { - schema.getExtensions().remove("x-tb-own-props"); - if (schema.getExtensions().isEmpty()) { - schema.setExtensions(null); - } - } + // in the referenced parent schema to avoid duplication (e.g. EntityId children). + schemas.forEach((schemaName, schema) -> { + Set ownProps = schemaOwnProps.getOrDefault(schemaName, Set.of()); + deduplicateAllOfProperties(schema, schemas, ownProps); + }); + + // Reorder properties for all component schemas. This runs after all + // schemas are finalized so it covers schemas the ModelConverter only + // saw as a $ref (e.g. interface-based discriminator types like EntityId). + schemas.forEach((schemaName, schema) -> { + List propOrder = schemaPropertyOrders.getOrDefault(schemaName, List.of()); + reorderSchemaProperties(schema, propOrder); }); // Fix polymorphic request/response bodies: replace inline oneOf with base type $ref @@ -771,22 +737,11 @@ public class SwaggerConfiguration { } @SuppressWarnings("unchecked") - private void deduplicateAllOfProperties(Schema schema, Map allSchemas) { + private void deduplicateAllOfProperties(Schema schema, Map allSchemas, Set ownProps) { if (schema.getAllOf() == null) { return; } - // Properties declared in the class's own fields (not inherited from a superclass). - // These must NOT be stripped from the inline even if they also appear in a parent - // schema (e.g. a field that also has a corresponding interface getter in the parent). - Set ownProps = new LinkedHashSet<>(); - if (schema.getExtensions() != null - && schema.getExtensions().get("x-tb-own-props") instanceof List list) { - for (Object v : list) { - if (v instanceof String s) ownProps.add(s); - } - } - // Collect properties defined in any $ref'd parent within the allOf, recursively // walking the ancestor chain (each parent may itself use allOf to extend a grandparent). Set parentProperties = new LinkedHashSet<>(); @@ -856,6 +811,33 @@ public class SwaggerConfiguration { return own; } + @SuppressWarnings("unchecked") + private static void reorderSchemaProperties(Schema schema, List propOrder) { + if (schema.getProperties() != null && schema.getProperties().size() > 1) { + schema.setProperties(reorderProperties(schema.getProperties(), propOrder)); + } + if (schema.getAllOf() != null) { + for (Schema allOfElement : schema.getAllOf()) { + if (allOfElement.get$ref() != null) continue; + if (allOfElement.getProperties() != null && allOfElement.getProperties().size() > 1) { + allOfElement.setProperties(reorderProperties(allOfElement.getProperties(), propOrder)); + } + } + } + } + + private static LinkedHashMap reorderProperties(Map current, List propOrder) { + var reordered = new LinkedHashMap(); + for (String name : propOrder) { + Schema prop = current.get(name); + if (prop != null) reordered.put(name, prop); + } + // Any properties not covered by propOrder are appended + // alphabetically to guarantee a deterministic stable order. + new TreeMap<>(current).forEach((k, v) -> reordered.putIfAbsent(k, v)); + return reordered; + } + /** * Resolves the property ordering for a schema class. * diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 269e02818a..8007a77cb1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; import lombok.Builder; @@ -166,6 +167,7 @@ public class TwoFactorAuthController extends BaseController { @Builder public static class TwoFaProviderInfo { private TwoFaProviderType type; + @JsonProperty("default") private boolean isDefault; private String contact; private Integer minVerificationCodeSendPeriod; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java index 398f4b6929..1cbaa957f0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java @@ -33,7 +33,7 @@ import java.util.List; @Data @EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId", "resources"}) +@JsonPropertyOrder({"id", "createdTime", "tenantId", "fqn", "name", "deprecated", "scada", "version", "descriptor", "image", "description", "tags", "externalId", "resources"}) public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity { @Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") From 03f07ec24de8c418e562d72fe2007a43035a2be4 Mon Sep 17 00:00:00 2001 From: wbg1991 Date: Sun, 15 Mar 2026 14:23:02 +0900 Subject: [PATCH 158/313] Update Korean locale translations --- .../assets/locale/locale.constant-ko_KR.json | 9477 +++++++++++++++-- 1 file changed, 8664 insertions(+), 813 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index 787d26ee2e..ab9432a169 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -10,11 +10,16 @@ "permission-denied": "권한이 없습니다", "permission-denied-text": "이 작업을 수행할 권한이 없습니다!" }, + "account": { + "account": "계정", + "notification-settings": "알림 설정" + }, "action": { "activate": "활성화", "suspend": "비활성화", "save": "저장", "saveAs": "다른 이름으로 저장", + "move": "이동", "cancel": "취소", "ok": "확인", "delete": "삭제", @@ -35,6 +40,7 @@ "edit-mode": "수정 모드", "enter-edit-mode": "수정 모드 진입", "decline-changes": "변경사항 포기", + "decline": "취소", "close": "닫기", "back": "뒤로", "run": "실행", @@ -55,10 +61,29 @@ "continue": "계속", "discard-changes": "변경 취소", "download": "다운로드", + "next": "다음", "next-with-label": "다음: {{label}}", "read-more": "더보기", "hide": "숨기기", - "done": "완료" + "test": "테스트", + "done": "완료", + "print": "인쇄", + "restore": "복원", + "confirm": "확인", + "more": "더보기", + "less": "줄이기", + "skip": "건너뛰기", + "send": "보내기", + "reset": "초기화", + "show-more": "더보기", + "dont-show-again": "다시 표시하지 않음", + "see-documentation": "문서 보기", + "see-debug-events": "디버그 이벤트 보기", + "clear": "지우기", + "upload": "업로드", + "delete-anyway": "무시하고 삭제", + "delete-selected": "선택 삭제", + "set": "설정" }, "aggregation": { "aggregation": "집합", @@ -73,8 +98,11 @@ "none": "없음" }, "admin": { + "settings": "설정", "general": "일반", "general-settings": "일반 설정", + "home-settings": "홈 설정", + "home": "홈", "outgoing-mail": "메일 전송", "outgoing-mail-settings": "메일 전송 설정", "system-settings": "시스템 설정", @@ -83,6 +111,23 @@ "base-url-required": "기본 URL을 입력해야 합니다.", "prohibit-different-url": "클라이언트 요청 헤더로부터 호스트 이름 사용을 제한", "prohibit-different-url-hint": "이 설정은 프로덕션 환경에서 활성화되어야 합니다. 비활성화되면 보안 문제가 발생할 수 있습니다.", + "device-connectivity": { + "device-connectivity": "장치 연결", + "http-s": "HTTP(s)", + "mqtt-s": "MQTT(s)", + "coap-s": "COAP(s)", + "http": "HTTP", + "https": "HTTPS", + "mqtt": "MQTT", + "mqtts": "MQTTs", + "coap": "COAP", + "coaps": "COAPs", + "hint": "호스트 또는 포트 필드가 비어 있으면 기본 프로토콜 값이 사용됩니다.", + "host": "호스트", + "port": "포트", + "port-pattern": "포트는 양의 정수여야 합니다.", + "port-range": "포트를 1~65535 범위 내에서 입력하세요." + }, "mail-from": "보내는 사람", "mail-from-required": "보내는 사람을 입력해야 합니다.", "smtp-protocol": "SMTP 프로토콜", @@ -95,7 +140,7 @@ "timeout-required": "제한시간이 입력되지 않았습니다.", "timeout-invalid": "제한시간이 올바르게 입력되지 않았습니다.", "enable-tls": "TLS 사용", - "tls-version" : "TLS 버전", + "tls-version": "TLS 버전", "enable-proxy": "프록시 사용", "proxy-host": "프록시 호스트", "proxy-host-required": "프록시 호스트를 입력하세요.", @@ -104,6 +149,7 @@ "proxy-port-range": "프록시 포트를 1~65535 범위 내에서 입력하세요.", "proxy-user": "프록시 사용자", "proxy-password": "프록시 비밀번호", + "change-password": "비밀번호 변경", "send-test-mail": "테스트 메일 보내기", "sms-provider": "SMS 제공자", "sms-provider-settings": "SMS 제공자 설정", @@ -111,18 +157,21 @@ "sms-provider-type-required": "SMS 제공자 유형을 입력하세요.", "sms-provider-type-aws-sns": "Amazon SNS", "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", "aws-access-key-id": "AWS 액세스 키 ID", "aws-access-key-id-required": "AWS 액세스 ID를 입력하세요.", "aws-secret-access-key": "AWS 보안 액세스 키", "aws-secret-access-key-required": "AWS 보안 액세스 키를 입력하세요", "aws-region": "AWS 지역", "aws-region-required": "AWS 지역을 입력하세요", - "number-from": "Phone Number From", - "number-from-required": "Phone Number From을(를) 입력하세요.", - "number-to": "Phone Number To", - "number-to-required": "Phone Number To을(를) 입력하세요.", - "phone-number-hint": "Phone Number in E.164 format, ex. +19995550123", - "phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.", + "number-from": "발신 전화번호", + "number-from-required": "발신 전화번호를 입력하세요.", + "number-to": "수신 전화번호", + "number-to-required": "수신 전화번호를 입력하세요.", + "phone-number-hint": "E.164 형식의 전화번호 (예: +821012345678)", + "phone-number-hint-twilio": "전화번호는 E.164 형식/전화번호 SID/메시징 서비스 SID이어야 합니다. 예: +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "잘못된 전화번호 형식입니다. E.164 형식을 사용하세요 (예: +821012345678).", + "phone-number-pattern-twilio": "잘못된 전화번호입니다. E.164 형식/전화번호 SID/메시징 서비스 SID이어야 합니다. 예: +19995550123/PNXXX/MGXXX", "sms-message": "SMS 메시지", "sms-message-required": "SMS 메시지를 입력하세요.", "sms-message-max-length": "SMS 메시지는 1600자를 넘을 수 없습니다.", @@ -137,6 +186,9 @@ "minimum-password-length": "최소 비밀번호 길이", "minimum-password-length-required": "최소 비밀번호 길이를 입력하세요", "minimum-password-length-range": "최소 비밀번호 길이를 5~50 범위 내에서 입력하세요", + "maximum-password-length": "최대 비밀번호 길이", + "maximum-password-length-min": "최대 비밀번호 길이는 최소 6자 이상이어야 합니다.", + "maximum-password-length-less-min": "최대 비밀번호 길이는 최소 길이보다 커야 합니다.", "minimum-uppercase-letters": "최소 대문자 수", "minimum-uppercase-letters-range": "최소 대문자 수는 음수가 될 수 없습니다", "minimum-lowercase-letters": "최소 소문자 수", @@ -149,83 +201,372 @@ "password-expiration-period-days-range": "비밀번호 유효 주기는 음수가 될 수 없습니다", "password-reuse-frequency-days": "비밀번호 재사용 주기(일)", "password-reuse-frequency-days-range": "비밀번호 재사용 주기는 음수가 될 수 없습니다", + "allow-whitespace": "공백 문자 허용", + "force-reset-password-if-no-valid": "유효하지 않은 경우 비밀번호 재설정 강제", + "force-reset-password-if-no-valid-hint": "이 기능을 활성화할 때 주의하십시오. 유효하지 않은 비밀번호를 사용하는 사용자는 이메일을 통해 비밀번호를 재설정해야 합니다.", "general-policy": "일반 정책", "max-failed-login-attempts": "계정이 잠기기 전까지 허용되는 최대 로그인 실패 시도 수", "minimum-max-failed-login-attempts-range": "최대 로그인 실패 시도 수는 음수가 될 수 없습니다", "user-lockout-notification-email": "계정이 잠길 경우 이메일로 안내 발송", + "user-activation-token-ttl": "사용자 활성화 링크 TTL (시간)", + "user-activation-token-ttl-range": "사용자 활성화 링크 TTL은 1~24시간 범위여야 합니다.", + "password-reset-token-ttl": "비밀번호 재설정 링크 TTL (시간)", + "password-reset-token-ttl-range": "비밀번호 재설정 링크 TTL은 1~24시간 범위여야 합니다.", + "mobile-secret-key-length": "모바일 비밀 키 길이", + "mobile-secret-key-length-range": "모바일 비밀 키 길이는 양수여야 합니다.", "domain-name": "도메인 이름", "domain-name-unique": "도메인 이름과 프로토콜은 중복되지 않아야 합니다.", + "domain-name-max-length": "도메인 이름은 256자 미만이어야 합니다.", "error-verification-url": "도메인 이름은 '/'이나 ':'와 같은 문자를 포함할 수 없습니다. 예: thingsboard.io", + "connection-settings": "연결 설정", "oauth2": { - "access-token-uri": "액세스 토큰 URI", - "access-token-uri-required": "Access token URI를 입력하세요.", - "activate-user": "사용자 활성화", - "add-domain": "도메인 추가", - "delete-domain": "도메인 삭제", - "add-provider": "제공자 추가", - "delete-provider": "제공자 삭제", - "allow-user-creation": "사용자 생성 허용", - "always-fullscreen": "항상 최대화면으로", - "authorization-uri": "Authorization URI", - "authorization-uri-required": "Authorization URI를 입력하세요.", - "client-authentication-method": "클라이언트 authentication 방법", - "client-id": "Client ID", - "client-id-required": "Client ID를 입력하세요.", - "client-secret": "Client secret", - "client-secret-required": "Client secret을(를) 입력하세요.", - "custom-setting": "사용자 정의 설정", - "customer-name-pattern": "Customer name pattern", - "default-dashboard-name": "Default dashboard name", - "delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.", - "delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?", - "delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.", - "delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?", - "email-attribute-key": "Email 속성 키", - "email-attribute-key-required": "Email attribute key을(를) 입력하세요.", - "first-name-attribute-key": "이름 속성 키", - "general": "일반", - "jwk-set-uri": "JSON Web Key URI", - "last-name-attribute-key": "Last name attribute key", - "login-button-icon": "Login button icon", - "login-button-label": "Provider label", - "login-button-label-placeholder": "Login with $(Provider label)", - "login-button-label-required": "Label을(를) 입력하세요.", - "login-provider": "로그인 제공자", - "mapper": "Mapper", - "new-domain": "새로운 도메인", - "oauth2": "OAuth2", - "redirect-uri-template": "Redirect URI 탬플릿", - "copy-redirect-uri": "리디렉션 URI 복사", - "registration-id": "Registration ID", - "registration-id-required": "Registration ID을(를) 입력하세요.", - "registration-id-unique": "Registration ID는 시스템 상에서 중복될 수 없습니다.", - "scope": "Scope", - "scope-required": "Scope을(를) 입력하세요.", - "tenant-name-pattern": "테넌트 이름 규칙", - "tenant-name-pattern-required": "테넌트 이름 규칙을 입력하세요.", - "tenant-name-strategy": "Tenant name strategy", - "type": "Mapper type", - "uri-pattern-error": "유효하지 않은 URI 형식입니다.", - "url": "URL", - "url-pattern": "유효하지 않은 URL 형식입니다.", - "url-required": "URL을(를) 입력하세요.", - "user-info-uri": "User info URI", - "user-info-uri-required": "사용자 정보 URI을(를) 입력하세요.", - "user-name-attribute-name": "사용자 이름 속성 키", - "user-name-attribute-name-required": "사용자 이름 속성 키를 입력하세요", - "protocol": "Protocol", - "domain-schema-http": "HTTP", - "domain-schema-https": "HTTPS", - "domain-schema-mixed": "HTTP+HTTPS", - "enable": "OAuth2 설정 사용" - } + "access-token-uri": "액세스 토큰 URI", + "access-token-uri-required": "액세스 토큰 URI를 입력하세요.", + "activate-user": "사용자 활성화", + "add-domain": "도메인 추가", + "delete-domain": "도메인 삭제", + "add-provider": "제공자 추가", + "delete-provider": "제공자 삭제", + "allow-user-creation": "사용자 생성 허용", + "always-fullscreen": "항상 최대화면으로", + "authorization-uri": "인증 URI", + "authorization-uri-required": "인증 URI를 입력하세요.", + "add-client": "OAuth 2.0 클라이언트 추가", + "client-details": "OAuth 2.0 클라이언트 세부 정보", + "client": "OAuth 2.0 클라이언트", + "clients": "OAuth 2.0 클라이언트", + "no-oauth2-clients": "OAuth 2.0 클라이언트가 없습니다", + "search-oauth2-clients": "OAuth 2.0 클라이언트 검색", + "delete-client-title": "OAuth 2.0 클라이언트 '{{clientName}}'을(를) 삭제하시겠습니까?", + "delete-client-text": "주의: 확인 후 클라이언트와 모든 관련 데이터를 사용할 수 없게 됩니다.", + "delete-mobile-app-title": "모바일 애플리케이션 '{{applicationName}}'을(를) 삭제하시겠습니까?", + "delete-mobile-app-text": "주의: 확인 후 모바일 애플리케이션과 모든 관련 데이터를 사용할 수 없게 됩니다.", + "title": "제목", + "client-title-required": "제목은 필수 항목입니다", + "client-title-max-length": "제목은 100자 미만이어야 합니다.", + "advanced-settings": "고급 설정", + "domain-details": "도메인 세부 정보", + "no-domains": "도메인이 없습니다", + "search-domains": "도메인 검색", + "mobile-app-details": "모바일 애플리케이션 세부 정보", + "add-mobile-app": "모바일 애플리케이션 추가", + "no-mobile-apps": "모바일 애플리케이션이 없습니다", + "search-mobile-apps": "모바일 애플리케이션 검색", + "send-token": "토큰 전송", + "create-new": "새로 만들기", + "client-authentication-method": "클라이언트 인증 방법", + "client-id": "클라이언트 ID", + "client-id-required": "클라이언트 ID를 입력하세요.", + "client-id-max-length": "클라이언트 ID는 256자 미만이어야 합니다.", + "client-secret": "클라이언트 비밀키", + "client-secret-required": "클라이언트 비밀키를 입력하세요.", + "client-secret-max-length": "클라이언트 비밀키는 2049자 미만이어야 합니다.", + "custom-setting": "사용자 정의 설정", + "customer-name-pattern": "고객 이름 패턴", + "customer-name-pattern-max-length": "고객 이름 패턴은 256자 미만이어야 합니다.", + "default-dashboard-name": "기본 대시보드 이름", + "default-dashboard-name-max-length": "기본 대시보드 이름은 256자 미만이어야 합니다.", + "delete-domain-text": "주의: 확인 후 도메인과 모든 공급자 데이터를 사용할 수 없게 됩니다.", + "delete-domain-title": "'{{domainName}}' 도메인을 삭제하시겠습니까?", + "delete-registration-text": "주의: 확인 후 공급자 데이터를 사용할 수 없게 됩니다.", + "delete-registration-title": "'{{name}}' 제공자를 삭제하시겠습니까?", + "email-attribute-key": "이메일 속성 키", + "email-attribute-key-required": "이메일 속성 키를 입력하세요.", + "email-attribute-key-max-length": "이메일 속성 키는 32자 미만이어야 합니다.", + "first-name-attribute-key": "이름 속성 키", + "first-name-attribute-key-max-length": "이름 속성 키는 32자 미만이어야 합니다.", + "general": "일반", + "jwk-set-uri": "JSON 웹 키 URI", + "last-name-attribute-key": "성 속성 키", + "last-name-attribute-key-max-length": "성 속성 키는 32자 미만이어야 합니다.", + "login-button-icon": "로그인 버튼 아이콘", + "login-button-label": "제공자 레이블", + "login-button-label-placeholder": "$(제공자 레이블)로 로그인", + "login-button-label-required": "제공자 레이블을 입력하세요.", + "login-provider": "로그인 제공자", + "mapper": "매퍼", + "new-domain": "새로운 도메인", + "oauth2": "OAuth2", + "password-max-length": "비밀번호는 256자 미만이어야 합니다.", + "redirect-uri-template": "리디렉션 URI 탬플릿", + "copy-redirect-uri": "리디렉션 URI 복사", + "registration-id": "등록 ID", + "registration-id-required": "등록 ID를 입력하세요.", + "registration-id-unique": "등록 ID는 시스템 상에서 중복될 수 없습니다.", + "scope": "범위", + "scope-required": "범위를 입력하세요.", + "tenant-name-pattern": "테넌트 이름 규칙", + "tenant-name-pattern-required": "테넌트 이름 규칙을 입력하세요.", + "tenant-name-pattern-max-length": "테넌트 이름 패턴은 256자 미만이어야 합니다.", + "tenant-name-strategy": "테넌트 이름 전략", + "type": "매퍼 유형", + "uri-pattern-error": "유효하지 않은 URI 형식입니다.", + "url": "URL", + "url-pattern": "유효하지 않은 URL 형식입니다.", + "url-required": "URL을(를) 입력하세요.", + "url-max-length": "URL은 256자 미만이어야 합니다.", + "user-info-uri": "사용자 정보 URI", + "user-info-uri-required": "사용자 정보 URI을(를) 입력하세요.", + "username-max-length": "사용자 이름은 256자 미만이어야 합니다.", + "user-name-attribute-name": "사용자 이름 속성 키", + "user-name-attribute-name-required": "사용자 이름 속성 키를 입력하세요", + "protocol": "프로토콜", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "OAuth2 설정 사용", + "disable": "OAuth 2.0 설정 비활성화", + "edge": "엣지에 전파", + "edge-enable": "엣지 전파 활성화", + "edge-disable": "엣지 전파 비활성화", + "domains": "도메인", + "mobile-apps": "모바일 애플리케이션", + "mobile-package": "애플리케이션 패키지", + "mobile-package-placeholder": "예: my.example.app", + "mobile-package-hint": "Android: 고유한 애플리케이션 ID. iOS: 제품 번들 식별자.", + "mobile-package-unique": "애플리케이션 패키지는 고유해야 합니다.", + "mobile-package-required": "애플리케이션 패키지가 필요합니다.", + "mobile-package-max-length": "애플리케이션 패키지는 256자 미만이어야 합니다", + "mobile-package-spaces": "애플리케이션 패키지에는 공백이 포함되지 않아야 합니다.", + "mobile-app-secret": "애플리케이션 비밀키", + "mobile-app-secret-hint": "최소 512비트 데이터를 나타내는 Base64 인코딩 문자열.", + "mobile-app-secret-required": "애플리케이션 비밀키는 필수 항목입니다.", + "mobile-app-secret-min-length": "애플리케이션 비밀키는 최소 512비트여야 합니다.", + "mobile-app-secret-base64": "애플리케이션 비밀키는 Base64 형식이어야 합니다.", + "invalid-mobile-app-secret": "애플리케이션 비밀키는 영숫자만 포함해야 하며", + "copy-mobile-app-secret": "애플리케이션 비밀키 복사", + "delete-mobile-app": "애플리케이션 정보 삭제", + "providers": "제공자", + "platform-web": "웹", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "모든 플랫폼", + "smtp-provider": "SMTP 제공자", + "allowed-platforms": "허용된 플랫폼", + "authentication": "인증", + "basic": "기본", + "provider": "제공자", + "redirect-url": "리디렉션 URI", + "domain-name": "도메인 이름", + "domain-name-required": "도메인 이름은 필수 항목입니다.", + "redirect-url-template": "리디렉션 URI 탬플릿", + "microsoft-tenant-id": "디렉터리 (테넌트) ID", + "microsoft-tenant-id-required": "디렉터리 (테넌트) ID는 필수 항목입니다.", + "token-uri": "토큰 URI", + "token-uri-required": "토큰 URI는 필수 항목입니다.", + "redirect-uri": "리디렉션 URI", + "google-provider": "Google", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", + "custom-provider": "사용자 지정", + "generate-access-token": "액세스 토큰 생성", + "update-access-token": "액세스 토큰 업데이트", + "access-token-status": "액세스 토큰 상태:", + "token-status-generated": "생성됨", + "token-status-not-generated": "생성되지 않음" + }, + "smpp-provider": { + "smpp-version": "SMPP 버전", + "smpp-host": "SMPP 호스트", + "smpp-host-required": "SMPP 호스트는 필수 항목입니다.", + "smpp-port": "SMPP 포트", + "smpp-port-required": "SMPP 포트는 필수 항목입니다.", + "system-id": "시스템 ID", + "system-id-required": "시스템 ID는 필수 항목입니다.", + "password": "비밀번호", + "password-required": "비밀번호는 필수 항목입니다.", + "type-settings": "유형 설정", + "source-settings": "소스 설정", + "destination-settings": "대상 설정", + "additional-settings": "추가 설정", + "system-type": "시스템 유형", + "bind-type": "바인드 유형", + "service-type": "서비스 유형", + "source-address": "소스 주소", + "source-ton": "소스 TON", + "source-npi": "소스 NPI", + "destination-ton": "대상 TON (번호 유형)", + "destination-npi": "대상 NPI (번호 계획 식별)", + "address-range": "주소 범위", + "coding-scheme": "코딩 체계", + "bind-type-tx": "송신기", + "bind-type-rx": "수신기", + "bind-type-trx": "송수신기", + "ton-unknown": "알 수 없음", + "ton-international": "국제", + "ton-national": "국내", + "ton-network-specific": "네트워크 특정", + "ton-subscriber-number": "가입자 번호", + "ton-alphanumeric": "영숫자", + "ton-abbreviated": "약어", + "npi-unknown": "0 - 알 수 없음", + "npi-isdn": "1 - ISDN/전화 번호 계획 (E163/E164)", + "npi-data-numbering-plan": "3 - 데이터 번호 계획 (X.121)", + "npi-telex-numbering-plan": "4 - 텔렉스 번호 계획 (F.69)", + "npi-land-mobile": "6 - 육상 이동통신 (E.212)", + "npi-national-numbering-plan": "8 - 국가 번호 계획", + "npi-private-numbering-plan": "9 - 사설 번호 계획", + "npi-ermes-numbering-plan": "10 - ERMES 번호 계획 (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - 인터넷 (IP)", + "npi-wap-client-id": "18 - WAP 클라이언트 ID (WAP 포럼에서 정의 예정)", + "scheme-smsc": "0 - SMSC 기본 알파벳 (단문 및 장문 코드용 ASCII, 무료용 GSM)", + "scheme-ia5": "1 - IA5 (단문 및 장문 코드용 ASCII, 무료용 Latin 9 (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - 옥텟 미지정 (8비트 바이너리)", + "scheme-latin-1": "3 - 라틴 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - 옥텟 미지정 (8비트 바이너리)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - 키릴 문자 (ISO-8859-5)", + "scheme-latin-hebrew": "7 - 라틴/히브리어 (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - 픽토그램 인코딩", + "scheme-music-codes": "10 - 음악 코드 (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - 확장 칸지 JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - 한국어 그래픽 문자 세트 (KS C 5601/KS X 1001)" + }, + "queue-select-name": "큐 이름 선택", + "queue-name": "이름", + "queue-name-required": "큐 이름은 필수 항목입니다!", + "queues": "큐", + "queue-partitions": "파티션", + "queue-submit-strategy": "제출 전략", + "queue-processing-strategy": "처리 전략", + "queue-configuration": "큐 구성", + "repository-settings": "저장소 설정", + "repository": "저장소", + "repository-url": "저장소 URL", + "repository-url-required": "저장소 URL은 필수 항목입니다.", + "default-branch": "기본 브랜치 이름", + "repository-read-only": "읽기 전용", + "show-merge-commits": "병합 커밋 표시", + "authentication-settings": "인증 설정", + "auth-method": "인증 방법", + "auth-method-username-password": "비밀번호 / 액세스 토큰", + "auth-method-username-password-hint": "GitHub 사용자는 액세스 토큰을 사용해야 합니다", + "auth-method-private-key": "개인 키", + "password-access-token": "비밀번호 / 액세스 토큰", + "change-password-access-token": "비밀번호 / 액세스 토큰 변경", + "private-key": "개인 키", + "drop-private-key-file-or": "개인 키 파일을 끌어다 놓거나", + "passphrase": "암호 문구", + "enter-passphrase": "암호 문구 입력", + "change-passphrase": "암호 문구 변경", + "check-access": "액세스 확인", + "check-repository-access-success": "저장소 액세스가 성공적으로 확인되었습니다!", + "delete-repository-settings-title": "저장소 설정을 삭제하시겠습니까?", + "delete-repository-settings-text": "주의: 확인 후 저장소 설정이 제거되고", + "auto-commit-settings": "자동 커밋 설정", + "auto-commit": "자동 커밋", + "auto-commit-entities": "자동 커밋 엔티티", + "no-auto-commit-entities-prompt": "자동 커밋에 구성된 엔티티가 없습니다", + "delete-auto-commit-settings-title": "자동 커밋 설정을 삭제하시겠습니까?", + "delete-auto-commit-settings-text": "주의: 확인 후에는 자동 커밋 설정이 삭제되며, 모든 엔티티의 자동 커밋 기능이 비활성화됩니다.", + "mobile-app": { + "mobile-app": "모바일 앱", + "mobile-app-qr-code-widget-settings": "모바일 앱 QR 코드 위젯 설정", + "applications": "애플리케이션", + "default": "기본값", + "custom": "사용자 지정", + "android": "Android", + "ios": "iOS", + "appearance": "모양", + "appearance-on-home-page": "홈 페이지 모양", + "enabled": "사용", + "disabled": "비활성화됨", + "badges": "배지", + "label": "라벨", + "label-required": "레이블은 필수 항목입니다", + "label-max-length": "레이블은 50자 이하여야 합니다.", + "right": "오른쪽", + "left": "왼쪽", + "set": "설정", + "preview": "미리보기", + "connect-mobile-app": "모바일 앱 연결", + "use-system-settings": "시스템 설정 사용" + }, + "2fa": { + "2fa": "2단계 인증", + "available-providers": "사용 가능한 제공자", + "available-providers-required": "최소 하나의 2FA 제공자를 구성해야 합니다.", + "issuer-name": "발급자 이름", + "issuer-name-required": "발급자 이름은 필수 항목입니다.", + "max-verification-failures-before-user-lockout": "사용자 잠금 전 최대 인증 실패 횟수", + "max-verification-failures-before-user-lockout-pattern": "최대 인증 실패 횟수는 양의 정수여야 합니다.", + "number-of-checking-attempts": "확인 시도 횟수", + "number-of-checking-attempts-pattern": "확인 시도 횟수는 양의 정수여야 합니다.", + "number-of-checking-attempts-required": "확인 시도 횟수는 필수 항목입니다.", + "number-of-codes": "코드 수", + "number-of-codes-pattern": "코드 수는 양의 정수여야 합니다.", + "number-of-codes-required": "코드 수는 필수 항목입니다.", + "provider": "제공자", + "retry-verification-code-period": "인증 코드 재시도 기간", + "retry-verification-code-period-pattern": "최소 기간은 5초입니다.", + "retry-verification-code-period-required": "인증 코드 재시도 기간은 필수 항목입니다.", + "total-allowed-time-for-verification": "인증 총 허용 시간", + "total-allowed-time-for-verification-pattern": "최소 총 허용 시간은 60초입니다.", + "total-allowed-time-for-verification-required": "총 허용 시간은 필수 항목입니다.", + "use-system-two-factor-auth-settings": "시스템 2단계 인증 설정 사용", + "verification-code-check-rate-limit": "인증 코드 확인 속도 제한", + "verification-code-lifetime": "인증 코드 수명", + "verification-code-lifetime-pattern": "인증 코드 수명은 양의 정수여야 합니다.", + "verification-code-lifetime-required": "인증 코드 수명은 필수 항목입니다.", + "verification-message-template": "인증 메시지 템플릿", + "verification-limitations": "인증 제한", + "verification-message-template-pattern": "인증 메시지에는 ${code} 패턴이 포함되어야 합니다.", + "verification-message-template-required": "인증 메시지 템플릿은 필수 항목입니다.", + "within-time": "시간 내", + "within-time-pattern": "시간은 양의 정수여야 합니다.", + "within-time-required": "시간은 필수 항목입니다.", + "force-2fa": "2단계 인증 강제", + "enforce-for": "강제 적용 대상" + }, + "jwt": { + "security-settings": "JWT 보안 설정", + "issuer-name": "발급자 이름", + "issuer-name-required": "발급자 이름은 필수 항목입니다.", + "signings-key": "서명 키", + "signings-key-hint": "최소 512비트 데이터를 나타내는 Base64 인코딩 문자열.", + "signings-key-required": "서명 키는 필수 항목입니다.", + "signings-key-min-length": "서명 키는 최소 512비트여야 합니다.", + "signings-key-base64": "서명 키는 Base64 형식이어야 합니다.", + "expiration-time": "토큰 만료 시간 (초)", + "expiration-time-required": "토큰 만료 시간은 필수 항목입니다.", + "expiration-time-max": "최대 허용 시간은 2147483647초 (68년)입니다.", + "expiration-time-min": "최소 시간은 60초 (1분)입니다.", + "refresh-expiration-time": "새로고침 토큰 만료 시간 (초)", + "refresh-expiration-time-required": "새로고침 토큰 만료 시간은 필수 항목입니다.", + "refresh-expiration-time-max": "최대 허용 시간은 2147483647초 (68년)입니다.", + "refresh-expiration-time-min": "최소 시간은 900초 (15분)입니다.", + "refresh-expiration-time-less-token": "새로고침 토큰 시간은 토큰 시간보다 커야 합니다.", + "generate-key": "키 생성", + "info-header": "모든 사용자가 다시 로그인해야 합니다", + "info-message": "JWT 서명 키 변경 시 발급된 모든 토큰이 무효화됩니다. 모든 사용자가" + }, + "resources": "리소스", + "notifications": "알림", + "notifications-settings": "알림 설정", + "slack-api-token": "Slack API 토큰", + "slack": "Slack", + "slack-settings": "Slack 설정", + "mobile-settings": "모바일 설정", + "firebase-service-account-file": "Firebase 서비스 계정 자격 증명 JSON 파일", + "select-firebase-service-account-file": "Firebase 서비스 계정 자격 증명 파일을 끌어다 놓거나", + "trendz": "Trendz", + "trendz-settings": "Trendz 설정", + "trendz-url": "Trendz URL", + "trendz-url-required": "Trendz URL은 필수 항목입니다", + "trendz-api-key": "Trendz API 키", + "trendz-enable": "Trendz 사용" }, "alarm": { "alarm": "알람", + "alarm-list": "알람 목록", "alarms": "알람", + "all-alarms": "모든 알람", "select-alarm": "알람 선택", "no-alarms-matching": "'{{entity}}'에 대한 알람이 존재하지 않습니다.", "alarm-required": "알람이 필요합니다", + "alarm-filter": "알람 필터", + "filter": "필터", "alarm-status": "알람 상태", "alarm-status-list": "알람 상태 목록", "any-status": "모든 상태", @@ -249,12 +590,26 @@ "originator": "만든 사람", "originator-type": "만든 사람 유형", "details": "상세 내역", + "originator-label": "발생자 라벨", + "assign": "할당", + "assignments": "할당", + "assignee": "담당자", + "assignee-id": "담당자 ID", + "assignee-first-name": "담당자 이름", + "assignee-last-name": "담당자 성", + "assignee-email": "담당자 이메일", + "unassigned": "할당 해제됨", + "user-deleted": "사용자 삭제됨", + "assignee-not-set": "모두", "status": "상태", "alarm-details": "알람 상세", "start-time": "시작 시각", + "assign-time": "할당 시간", "end-time": "마지막 시각", "ack-time": "확인 시간", "clear-time": "해제 시간", + "duration": "기간", + "alarm-severity": "알람 심각도", "alarm-severity-list": "알람 심각도 목록", "any-severity": "모든 심각도", "severity-critical": "심각한", @@ -264,6 +619,7 @@ "severity-indeterminate": "중간", "acknowledge": "확인", "clear": "지우기", + "delete": "삭제", "search": "알람 검색", "selected-alarms": "{ count, plural, =1 {1 개 알람} other {# 개 알람} }이 선택됨", "no-data": "표시할 데이터가 없습니다", @@ -274,67 +630,116 @@ "aknowledge-alarms-text": "{ count, plural, =1 {1 개 알람} other {# 개 알람} }의 알람을 수용하시겠습니까?", "aknowledge-alarm-title": "알람 확인", "aknowledge-alarm-text": "알람을 확인하시겠습니까??", + "selected-alarms-are-acknowledged": "선택된 알람은 이미 확인되었습니다", "clear-alarms-title": "{ count, plural, =1 {1 개 알람} other {# 개 알람} } 해제", "clear-alarms-text": "{ count, plural, =1 {1 개 알람} other {# 개 알람} }을 해제하시겠습니까?", "clear-alarm-title": "알람 해제", "clear-alarm-text": "알람을 해제하시겠습니까?", + "delete-alarms-title": "{ count, plural, =1 {1개 알람} other {#개 알람} } 삭제", + "delete-alarms-text": "{ count, plural, =1 {1개 알람} other {#개 알람} }을(를) 삭제하시겠습니까?", + "selected-alarms-are-cleared": "선택된 알람은 이미 해제되었습니다", "alarm-status-filter": "알람 상태 필터", - "alarm-filter": "알람 필터", + "alarm-filter-title": "알람 필터", + "assigned": "할당됨", + "filter-title": "필터", "max-count-load": "불러올 최대 알람 수를 0~무제한 범위 내에서 입력하세요", "max-count-load-required": "불러올 최대 알람 수를 입력하세요.", "max-count-load-error-min": "최소 값은 0입니다.", - "fetch-size": "Fetch size", - "fetch-size-required": "Fetch size를 입력하세요.", + "fetch-size": "페치 크기", + "fetch-size-required": "페치 크기를 입력하세요.", "fetch-size-error-min": "최소 값은 10입니다.", + "alarm-types": "알람 유형", "alarm-type-list": "알람 유형 목록", "any-type": "모든 유형", - "search-propagated-alarms": "전파된 알람 검색" + "assigned-to-current-user": "현재 사용자에게 할당됨", + "assigned-to-me": "나에게 할당됨", + "search-propagated-alarms": "전파된 알람 검색", + "comments": "알람 댓글", + "show-more": "더보기", + "additional-info": "추가 정보", + "alarm-type": "알람 유형", + "enter-alarm-type": "알람 유형 입력", + "no-alarm-types-matching": "'{{entitySubtype}}'과 일치하는 알람 유형이 없습니다.", + "alarm-type-list-empty": "선택된 알람 유형이 없습니다.", + "system-comments": { + "acked-by-user": "사용자 {{userName}}이(가) 알람을 확인했습니다", + "cleared-by-user": "사용자 {{userName}}이(가) 알람을 해제했습니다", + "assigned-to-user": "사용자 {{userName}}이(가) 알람을 사용자 {{assigneeName}}에게 할당했습니다", + "unassigned-to-user": "사용자 {{userName}}이(가) 알람 할당을 해제했습니다", + "unassigned-from-deleted-user": "사용자 {{userName}}이(가) 삭제되어 알람 할당이 해제되었습니다", + "comment-deleted": "사용자 {{userName}}이(가) 자신의 댓글을 삭제했습니다", + "severity-changed": "알람 심각도가 {{oldSeverity}}에서 {{newSeverity}}(으)로 업데이트되었습니다" + } + }, + "alarm-activity": { + "add": "댓글 추가...", + "alarm-comment": "알람 댓글", + "comments": "댓글", + "delete-alarm-comment": "이 댓글을 삭제하시겠습니까?", + "refresh": "새로고침", + "oldest-first": "오래된 순", + "newest-first": "최신 순", + "activity": "활동", + "export": "CSV로 내보내기", + "author": "작성자", + "created-date": "생성 날짜", + "edited-date": "수정 날짜", + "text": "텍스트", + "system": "시스템" }, "alias": { - "add": "별칭 추가", - "edit": "별칭 편집", - "name": "별칭", - "name-required": "별칭을 입력하세요.", - "duplicate-alias": "동일한 별칭이 이미 존재합니다.", - "filter-type-single-entity": "단일 개체", - "filter-type-entity-list": "개체 목록", - "filter-type-entity-name": "개체 이름", - "filter-type-state-entity": "대시보드 상태로 부터의 개체", - "filter-type-state-entity-description": "개체 taken from dashboard state parameters", + "add": "별명 추가", + "edit": "별명 편집", + "name": "별명", + "name-required": "별명을 입력하세요.", + "duplicate-alias": "동일한 별명이 이미 존재합니다.", + "filter-type-single-entity": "단일 엔티티", + "filter-type-entity-list": "엔티티 목록", + "filter-type-entity-name": "엔티티 이름", + "filter-type-entity-type": "엔티티 유형", + "filter-type-state-entity": "대시보드 상태로 부터의 엔티티", + "filter-type-state-entity-description": "대시보드 상태 파라미터에서 가져온 엔티티", "filter-type-asset-type": "자산 유형", "filter-type-asset-type-description": "'{{assetTypes}}' 유형의 자산", "filter-type-asset-type-and-name-description": "이름이 '{{prefix}}'로 시작되는 '{{assetTypes}}' 유형의 자산", "filter-type-device-type": "장치 유형", "filter-type-device-type-description": "'{{deviceTypes}}' 유형의 장치", "filter-type-device-type-and-name-description": "이름이 '{{prefix}}'로 시작되는 '{{deviceTypes}}' 유형의 장치", - "filter-type-entity-view-type": "Entity View type", - "filter-type-entity-view-type-description": "Entity Views of type '{{entityViewTypes}}'", - "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityViewTypes}}' and with name starting with '{{prefix}}'", + "filter-type-entity-view-type": "엔티티 뷰 유형", + "filter-type-entity-view-type-description": "유형 '{{entityViewTypes}}'의 엔티티 뷰", + "filter-type-entity-view-type-and-name-description": "유형이 '{{entityViewTypes}}'이고 이름이 '{{prefix}}'로 시작하는 엔티티 뷰", + "filter-type-edge-type": "엣지 유형", + "filter-type-edge-type-description": "유형 '{{edgeTypes}}'의 엣지", + "filter-type-edge-type-and-name-description": "유형 '{{edgeTypes}}'이고 이름이 '{{prefix}}'로 시작하는 엣지", "filter-type-relations-query": "관계 질의", - "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-relations-query-description": "{{rootEntity}} {{direction}} {{relationType}} 관계가 있는 {{entities}}", "filter-type-asset-search-query": "자산 검색 질의", - "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-asset-search-query-description": "{{rootEntity}} {{direction}} {{relationType}} 관계가 있는 유형 {{assetTypes}}의 자산", "filter-type-device-search-query": "장치 검색 질의", - "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "개체 보기 검색 질의", - "filter-type-entity-view-search-query-description": "Entity views with types {{entityViewTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-device-search-query-description": "{{rootEntity}} {{direction}} {{relationType}} 관계가 있는 유형 {{deviceTypes}}의 장치", + "filter-type-entity-view-search-query": "엔티티 뷰 검색 질의", + "filter-type-entity-view-search-query-description": "{{rootEntity}} {{direction}} {{relationType}} 관계가 있는 유형 {{entityViewTypes}}의 엔티티 뷰", "filter-type-apiUsageState": "Api 사용량 상태", - "entity-filter": "개체 필터", - "resolve-multiple": "Resolve as multiple entities", + "filter-type-edge-search-query": "엣지 검색 쿼리", + "filter-type-edge-search-query-description": "유형 {{edgeTypes}}이(가) {{relationType}} 관계 {{direction}}을(를) 가진 엣지", + "entity-filter": "엔티티 필터", + "resolve-multiple": "여러 엔티티로 확인", + "resolve-multiple-hint": "필터링된 모든 엔티티의 데이터를 동시에 렌더링하도록 활성화합니다.\n비활성화할 경우, 위젯에는 선택된 엔티티의 데이터만 표시됩니다.", "filter-type": "필터 유형", "filter-type-required": "필터 유형을 입력하세요.", - "entity-filter-no-entity-matched": "지정된 필터에 해당되는 개체가 없습니다.", - "no-entity-filter-specified": "개체 필터가 지정되지 않음", - "root-state-entity": "루트로 대시보드 상태 개체를 사용", - "last-level-relation": "Fetch last level relation only", - "root-entity": "루트 개체", - "state-entity-parameter-name": "상태 개체 파라미터 이름", - "default-state-entity": "기본 상태 개체", + "entity-filter-no-entity-matched": "지정된 필터에 해당되는 엔티티가 없습니다.", + "no-entity-filter-specified": "엔티티 필터가 지정되지 않음", + "root-state-entity": "루트로 대시보드 상태 엔티티를 사용", + "last-level-relation": "마지막 수준 관계만 가져오기", + "root-entity": "루트 엔티티", + "state-entity-parameter-name": "상태 엔티티 파라미터 이름", + "default-state-entity": "기본 상태 엔티티", "default-entity-parameter-name": "기본값", + "query-options": "쿼리 옵션", "max-relation-level": "최대 관계 수준", "unlimited-level": "제한 수준", - "state-entity": "대시보드 상태 개체", - "all-entities": "모든 개체", + "state-entity": "대시보드 상태 엔티티", + "all-entities": "모든 엔티티", "any-relation": "전체" }, "asset": { @@ -343,16 +748,17 @@ "management": "자산 관리", "view-assets": "자산 보기", "add": "자산 추가", - "assign-to-customer": "커스터머에게 자산 할당", - "assign-asset-to-customer": "자산을 커스터머에게 할당", - "assign-asset-to-customer-text": "커스터머에게 할당할 자산을 선택하세요", + "asset-type-max-length": "자산 유형은 256자 미만이어야 합니다.", + "assign-to-customer": "고객에게 자산 할당", + "assign-asset-to-customer": "자산을 고객에게 할당", + "assign-asset-to-customer-text": "고객에게 할당할 자산을 선택하세요", "no-assets-text": "아무 자산도 없습니다", - "assign-to-customer-text": "자산을 할당할 커스터머를 선택하세요", + "assign-to-customer-text": "자산을 할당할 고객를 선택하세요", "public": "공개", - "assignedToCustomer": "할당된 커스터머", + "assignedToCustomer": "할당된 고객", "make-public": "자산을 공개로 설정", "make-private": "자산을 비공개로 설정", - "unassign-from-customer": "커스터머 할당 해제", + "unassign-from-customer": "고객 할당 해제", "delete": "자산 삭제", "asset-public": "공개된 자산", "asset-type": "자산 유형", @@ -365,7 +771,10 @@ "asset-types": "자산 유형", "name": "이름", "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "label-max-length": "레이블은 256자 미만이어야 합니다.", "description": "설명", + "description-required": "설명은 필수 항목입니다.", "type": "유형", "type-required": "유형일 입력하세요.", "details": "상세 내역", @@ -373,10 +782,12 @@ "add-asset-text": "새로운 자산 추가", "asset-details": "자산 상세 내역", "assign-assets": "자산 할당", - "assign-assets-text": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 커스터머에게 할당", + "assign-assets-text": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 고객에게 할당", + "assign-asset-to-edge-title": "엣지에 자산 할당", + "assign-asset-to-edge-text": "엣지에 할당할 자산을 선택하세요", "delete-assets": "자산 삭제", "unassign-assets": "자산 할당 해제", - "unassign-assets-action-title": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 커스터머로부터 할당 해제", + "unassign-assets-action-title": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 고객로부터 할당 해제", "assign-new-asset": "새로운 자산 할당", "delete-asset-title": "자산 '{{assetName}}'을(를) 삭제하시겠습니까?", "delete-asset-text": "자산 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", @@ -388,37 +799,54 @@ "make-private-asset-title": "자산 '{{assetName}}'을(를) 비공개로 변경하시겠습니까?", "make-private-asset-text": "자산 및 관련 데이터가 비공개로 전환되면 다른 이들이 접근할 수 없게 됩니다.", "unassign-asset-title": "자산 '{{assetName}}'을 할당 해제 하시겠습니까?", - "unassign-asset-text": "자산이 할당 해제되면 커스터머가 접근할 수 없게 됩니다.", + "unassign-asset-text": "자산이 할당 해제되면 고객가 접근할 수 없게 됩니다.", "unassign-asset": "자산 할당 해제", "unassign-assets-title": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 할당 해제 하시겠습니까?", - "unassign-assets-text": "자산이 할당 해제되면 커스터머가 접근할 수 없게 됩니다.", + "unassign-assets-text": "자산이 할당 해제되면 고객가 접근할 수 없게 됩니다.", + "unassign-assets-from-edge": "엣지에서 자산 할당 해제", "copyId": "자산 ID 복사", "idCopiedMessage": "자산 ID가 클립보드로 복사되었습니다", "select-asset": "자산 선택", "no-assets-matching": "'{{entity}}'와 일치하는 자산이 없습니다.", "asset-required": "자산을 입력하세요", "name-starts-with": "다음으로 시작되는 자산: ", + "help-text": "필요에 따라 '%'를 사용하세요: '%asset_name_contains%', '%asset_name_ends', 'asset_s", "import": "자산 불러오기", "asset-file": "자산 파일", + "label": "라벨", "search": "자산 검색", - "selected-assets": "{ count, plural, =1 {1 개 자산} other {# 개 자산} } 선택됨", - "label": "라벨" + "assign-asset-to-edge": "엣지에 자산 할당", + "unassign-asset-from-edge": "자산 할당 해제", + "unassign-asset-from-edge-title": "자산 '{{assetName}}'을 할당 해제 하시겠습니까?", + "unassign-asset-from-edge-text": "확인 후 자산 할당이 해제되고", + "unassign-assets-from-edge-title": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }을 할당 해제 하시겠습니까?", + "unassign-assets-from-edge-text": "확인 후 선택한 모든 자산의 할당이 해제되고", + "selected-assets": "{ count, plural, =1 {1 개 자산} other {# 개 자산} } 선택됨" }, "attribute": { "attributes": "속성", "latest-telemetry": "최근 데이터", + "no-latest-telemetry": "최신 텔레메트리 없음", "attributes-scope": "장치 속성 범위", - "scope-telemetry": "원격 측정", + "scope-telemetry": "텔레메트리", "scope-latest-telemetry": "최근 데이터", "scope-client": "클라이언트 속성", "scope-server": "서버 속성", "scope-shared": "공유 속성", + "scope-client-short": "클라이언트", + "scope-server-short": "서버", + "scope-shared-short": "공유", + "scope-latest-short": "최신", + "scope-any": "전체", "add": "속성 추가", "key": "키", + "key-max-length": "키는 256자 미만이어야 합니다.", "last-update-time": "마지막 수정된 시간", "key-required": "속성 키를 입력하세요.", - "value": "Value", + "value": "값", "value-required": "속성 값을 입력하세요.", + "telemetry-key-required": "텔레메트리 키는 필수 항목입니다", + "telemetry-value-required": "텔레메트리 값은 필수 항목입니다", "delete-attributes-title": "{ count, plural, =1 {속성} other {여러 속성들을} } 삭제하시겠습니까??", "delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.", "delete-attributes": "속성 삭제", @@ -432,63 +860,179 @@ "selected-attributes": "{ count, plural, =1 {1 개 속성} other {# 개 속성} }이 선택됨", "selected-telemetry": "{ count, plural, =1 {1 개의 텔레메트리 단위} other {# 개의 텔레메트리 단위} }가 선택됨", "no-attributes-text": "아무 속성도 찾을 수 없습니다", - "no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다." + "no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다.", + "copy-key": "키 복사", + "add-telemetry": "텔레메트리 추가", + "copy-value": "값 복사", + "delete-timeseries": { + "start-time": "시작 시각", + "ends-on": "종료일", + "strategy": "전략", + "delete-strategy": "삭제 전략", + "all-data": "모든 데이터 삭제", + "all-data-except-latest-value": "최신 값을 제외한 모든 데이터 삭제", + "latest-value": "최신 값 삭제", + "all-data-for-time-period": "기간 내 모든 데이터 삭제", + "rewrite-latest-value": "최신 값 다시 쓰기" + } }, "api-usage": { + "api-features": "API 기능", "api-usage": "Api 사용량", + "alarm": "알람", + "alarms-created": "생성된 알람", + "queue-stats": "큐 통계", + "processing-failures-and-timeouts": "처리 실패 및 시간 초과", + "exceptions": "예외", + "alarms-created-daily-activity": "일일 알람 생성 활동", + "alarms-created-hourly-activity": "시간별 알람 생성 활동", + "alarms-created-monthly-activity": "월별 알람 생성 활동", "data-points": "데이터 포인트", "data-points-storage-days": "데이터 포인트 저장 일수", - "email": "Email", + "data-points-storage-days-hourly-activity": "데이터 포인트 저장 일수 시간별 활동", + "data-points-storage-days-daily-activity": "데이터 포인트 저장 일수 일별 활동", + "data-points-storage-days-monthly-activity": "데이터 포인트 저장 일수 월별 활동", + "device-api": "장치 API", + "email": "이메일", "email-messages": "Email 메시지", "email-messages-daily-activity": "일별 Email 메세지 활동", - "email-messages-hourly-activity": "시간별 Email 메시지 활동", "email-messages-monthly-activity": "월별 Email 메시지 활동", - "exceptions": "예외", - "executions": "Executions", + "executions": "실행", + "scripts": "스크립트", + "scripts-hourly-activity": "시간별 스크립트 활동", + "scripts-daily-activity": "일일 스크립트 활동", + "scripts-monthly-activity": "월별 스크립트 활동", "javascript": "JavaScript", - "javascript-executions": "JavaScript executions", + "javascript-executions": "JavaScript 실행", + "tbel": "TBEL", + "tbel-executions": "TBEL 실행", "latest-error": "최근 오류", "messages": "메시지", - "permanent-failures": "${entityName} Permanent Failures", - "permanent-timeouts": "${entityName} Permanent Timeouts", - "processing-failures": "${entityName} Processing Failures", - "processing-failures-and-timeouts": "Processing Failures and Timeouts", - "processing-timeouts": "${entityName} Processing Timeouts", - "queue-stats": "Queue Stats", + "notifications": "알림", + "notifications-email-sms": "알림 (이메일/SMS)", + "notifications-hourly-activity": "알림 시간별 활동", + "permanent-failures": "${entityName} 영구 실패", + "permanent-timeouts": "${entityName} 영구 시간 초과", + "processing-failures": "${entityName} 처리 실패", + "processing-timeouts": "${entityName} 처리 시간 초과", "rule-chain": "규칙 사슬", "rule-engine": "규칙 엔진", - "rule-engine-daily-activity": "Rule Engine daily activity", - "rule-engine-executions": "Rule Engine executions", - "rule-engine-hourly-activity": "Rule Engine hourly activity", - "rule-engine-monthly-activity": "Rule Engine monthly activity", - "rule-engine-statistics": "Rule Engine Statistics", + "rule-engine-executions": "규칙 엔진 실행", + "rule-engine-hourly-activity": "규칙 엔진 시간별 활동", + "rule-engine-daily-activity": "규칙 엔진 일별 활동", + "rule-engine-monthly-activity": "규칙 엔진 월별 활동", + "rule-engine-statistics": "규칙 엔진 통계", "rule-node": "규칙 노드", "sms": "SMS", "sms-messages": "SMS 메시지", - "sms-messages-daily-activity": "일별 SMS 메시지 활동", "sms-messages-hourly-activity": "시간별 SMS 메시지 활동", + "sms-messages-daily-activity": "일별 SMS 메시지 활동", "sms-messages-monthly-activity": "월별 SMS 메시지 활동", - "successful": "${entityName} Successful", + "successful": "${entityName} 성공", "telemetry": "텔레메트리", - "telemetry-persistence": "Telemetry persistence", - "telemetry-persistence-daily-activity": "Telemetry persistence daily activity", - "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", - "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", + "telemetry-persistence": "텔레메트리 저장", + "telemetry-persistence-daily-activity": "텔레메트리 저장 일별 활동", + "telemetry-persistence-hourly-activity": "텔레메트리 저장 시간별 활동", + "telemetry-persistence-monthly-activity": "텔레메트리 저장 월별 활동", "transport": "전송", - "transport-daily-activity": "Transport daily activity", - "transport-data-points": "Transport data points", - "transport-hourly-activity": "Transport hourly activity", - "transport-messages": "Transport messages", - "transport-monthly-activity": "Transport monthly activity", + "transport-msg-hourly-activity": "전송 메시지 시간별 활동", + "transport-msg-daily-activity": "전송 메시지 일별 활동", + "transport-msg-monthly-activity": "전송 메시지 월별 활동", + "transport-daily-activity": "전송 일별 활동", + "transport-data-points": "전송 데이터 포인트", + "transport-data-points-hourly-activity": "전송 데이터 포인트 시간별 활동", + "transport-data-points-daily-activity": "전송 데이터 포인트 일별 활동", + "transport-data-points-monthly-activity": "전송 데이터 포인트 월별 활동", "view-details": "상세 내역", - "view-statistics": "통계 보기" + "view-statistics": "통계 보기", + "transport-messages": "전송 메시지", + "transport-messages-hourly-activity": "전송 메시지 시간별 활동", + "transport-data-point-hourly-activity": "전송 데이터 포인트 시간별 활동", + "javascript-function-executions": "JavaScript 함수 실행", + "javascript-function-executions-hourly-activity": "JavaScript 함수 실행 시간별 활동", + "javascript-function-executions-daily-activity": "JavaScript 함수 실행 일별 활동", + "javascript-function-executions-monthly-activity": "JavaScript 함수 실행 월별 활동", + "tbel-function-executions": "TBEL 함수 실행", + "tbel-function-executions-hourly-activity": "TBEL 함수 실행 시간별 활동", + "tbel-function-executions-daily-activity": "TBEL 함수 실행 일별 활동", + "tbel-function-executions-monthly-activity": "TBEL 함수 실행 월별 활동", + "created-reports": "생성된 보고서", + "created-reports-hourly-activity": "생성된 보고서 시간별 활동", + "created-reports-daily-activity": "생성된 보고서 일별 활동", + "created-reports-monthly-activity": "생성된 보고서 월별 활동", + "emails": "이메일", + "emails-hourly-activity": "이메일 시간별 활동", + "emails-daily-activity": "이메일 일별 활동", + "emails-monthly-activity": "이메일 월별 활동", + "status": { + "enabled": "사용", + "disabled": "비활성화됨", + "warning": "경고" + } + }, + "api-limit": { + "cassandra-write-queries-core": "REST API Cassandra 쓰기 쿼리", + "cassandra-read-queries-core": "REST API 및 WS 텔레메트리 Cassandra 읽기 쿼리", + "cassandra-write-queries-rule-engine": "규칙 엔진 텔레메트리 Cassandra 쓰기 쿼리", + "cassandra-read-queries-rule-engine": "규칙 엔진 텔레메트리 Cassandra 읽기 쿼리", + "cassandra-write-queries-monolith": "Monolith 텔레메트리 Cassandra 쓰기 쿼리", + "cassandra-read-queries-monolith": "Monolith 텔레메트리 Cassandra 읽기 쿼리", + "entity-version-creation": "엔티티 버전 생성", + "entity-version-load": "엔티티 버전 불러오기", + "notification-requests": "알림 요청", + "notification-requests-per-rule": "규칙당 알림 요청", + "rest-api-requests": "REST API 요청", + "rest-api-requests-per-customer": "고객당 REST API 요청", + "transport-messages": "전송 메시지", + "transport-messages-per-device": "장치당 전송 메시지", + "transport-messages-per-gateway": "게이트웨이당 전송 메시지", + "transport-messages-per-gateway-device": "게이트웨이 장치당 전송 메시지", + "ws-updates-per-session": "세션당 WS 업데이트", + "edge-events": "엣지 이벤트", + "edge-events-per-edge": "엣지당 엣지 이벤트", + "edge-uplink-messages": "엣지 업링크 메시지", + "edge-uplink-messages-per-edge": "엣지당 업링크 메시지" + }, + "api-key": { + "api-key": "API 키", + "api-keys": "API 키", + "delete-api-key-title": "API 키 '{{name}}'을(를) 삭제하시겠습니까?", + "delete-api-key-text": "주의: 확인 후 키를 복구할 수 없습니다.", + "delete-api-keys-title": "{ count, plural, =1 {1개 API 키} other {#개 API 키} }를 삭제하시겠습니까?", + "delete-api-keys-text": "주의: 확인 후 선택한 모든 키를 복구할 수 없게 됩니다.", + "expiration-date": "만료 날짜", + "date": "날짜", + "description": "설명", + "disable": "비활성화", + "edit-description": "설명 수정", + "enable": "API 키 사용", + "expiration-time": "만료 시간", + "expiration-time-never": "없음", + "expiration-time-custom": "사용자 지정", + "generate": "생성", + "generate-title": "API 키 생성", + "generate-text": "참고: API 키는 생성된 사용자의 권한을 상속받습니다.", + "generated-api-key-title": "API 키가 생성되었습니다. 연결을 확인하세요!", + "generated-api-key-copy": "지금 API 키를 복사하여 저장하세요. 다시 볼 수 없습니다.", + "generated-api-key-command": "다음 지침에 따라 연결을 확인하세요. 결과적으로", + "generated-api-key-insecure-url": "보안되지 않은 HTTP 연결을 통해 명령을 실행하면 API 키가 암호화되지 않고 전송됩니다.", + "list": "{ count, plural, =1 {1개 API 키} other {#개 API 키 목록} }", + "manage": "관리", + "manage-api-keys": "API 키 관리", + "no-found": "API 키가 없습니다", + "selected-api-keys": "{ count, plural, =1 {1개 API 키} other {#개 API 키} } 선택됨", + "search": "API 키 검색", + "status": "상태", + "status-active": "활성", + "status-inactive": "비활성", + "status-expired": "만료됨" }, "audit-log": { "audit": "감사", "audit-logs": "감사 로그", "timestamp": "타임스탬프", - "entity-type": "개체 유형", - "entity-name": "개체 이름", + "entity-type": "엔티티 유형", + "entity-name": "엔티티 이름", "user": "사용자", "type": "유형", "status": "상태", @@ -498,10 +1042,12 @@ "type-updated": "수정됨", "type-attributes-updated": "속성이 수정되었습니다", "type-attributes-deleted": "속성이 삭제되었습니다", - "type-rpc-call": "RPC call", + "type-rpc-call": "RPC 호출", "type-credentials-updated": "자격 증명이 갱신되었습니다", - "type-assigned-to-customer": "커스터머에게 할당", - "type-unassigned-from-customer": "지정된 커스터머 해제", + "type-assigned-to-customer": "고객에게 할당", + "type-unassigned-from-customer": "지정된 고객 해제", + "type-assigned-to-edge": "엣지에 할당됨", + "type-unassigned-from-edge": "엣지에서 할당 해제됨", "type-activated": "활성", "type-suspended": "일시 중지", "type-credentials-read": "자격 증명 읽기", @@ -511,6 +1057,12 @@ "type-relations-delete": "모든 관계가 삭제되었습니다", "type-alarm-ack": "확인", "type-alarm-clear": "해제", + "type-alarm-delete": "알람 삭제됨", + "type-alarm-assign": "알람 할당됨", + "type-alarm-unassign": "알람 할당 해제", + "type-added-comment": "댓글 추가됨", + "type-updated-comment": "댓글 수정됨", + "type-deleted-comment": "댓글 삭제됨", "type-login": "로그인", "type-logout": "로그아웃", "type-lockout": "잠금", @@ -525,7 +1077,587 @@ "type-assigned-from-tenant": "테넌트로부터 지정", "type-assigned-to-tenant": "테넌트로 지정", "type-provision-success": "장치가 프로비전됨", - "type-provision-failure": "장치 프로비저닝이 실패했습니다" + "type-provision-failure": "장치 프로비저닝이 실패했습니다", + "type-timeseries-updated": "텔레메트리 수정됨", + "type-timeseries-deleted": "텔레메트리 삭제됨", + "type-sms-sent": "SMS 전송됨", + "any-type": "모든 유형", + "audit-log-filter-title": "감사 로그 필터", + "filter-title": "필터", + "filter-types": "감사 로그 유형" + }, + "debug-settings": { + "label": "디버그 설정", + "on-failure": "실패만 (24/7)", + "all-messages": "모든 메시지 ({{time}})", + "failures": "실패", + "entity": "엔티티", + "hint": { + "main-limited": "{{time}}당 최대 {{msg}}개의 {{entity}} 디버그 메시지가 기록됩니다.", + "on-failure": "오류 메시지만 기록합니다.", + "all-messages": "모든 디버그 메시지를 기록합니다." + } + }, + "calculated-fields": { + "expression": "표현식", + "no-found": "계산 필드가 없습니다", + "add": "계산 필드 추가", + "list": "{ count, plural, =1 {1개 계산 필드} other {#개 계산 필드 목록} }", + "selected-fields": "{ count, plural, =1 {1개 계산 필드} other {#개 계산 필드} } 선택됨", + "type": { + "simple": "간단", + "simple-hint": "입력 인수를 기반으로 한 단순 산술 계산.", + "script": "스크립트", + "script-hint": "TBEL 스크립트를 사용하여 정의된 인수에 대한 계산.", + "geofencing": "지오펜싱", + "geofencing-hint": "구성된 지오펜싱 영역 그룹에 대한 엔티티 GPS 위치 및 전환 평가.", + "propagation": "전파", + "propagation-hint": "관계 방향 및 유형에 따라 부모 또는 자식 엔티티로 데이터 전파.", + "related-entities-aggregation": "관련 엔티티 집계", + "related-entities-aggregation-hint": "관련 엔티티의 최신 데이터 집계.", + "time-series-data-aggregation": "시계열 데이터 집계", + "time-series-data-aggregation-hint": "현재 엔티티의 과거 데이터 집계." + }, + "preview": "미리보기", + "arguments": "인수", + "decimals-by-default": "기본 소수 자릿수", + "debugging": "계산 필드 디버깅", + "calculated-field-details": "계산 필드 상세", + "argument-name": "인수 이름", + "name": "이름", + "datasource": "데이터소스", + "add-argument": "인수 추가", + "test-script-function": "스크립트 함수 테스트", + "no-arguments": "최소 하나의 인수가 필요합니다.", + "argument-settings": "인수 설정", + "argument-current": "현재 엔티티", + "argument-current-tenant": "현재 테넌트", + "argument-device": "장치", + "argument-asset": "자산", + "argument-customer": "고객", + "argument-tenant": "현재 테넌트", + "argument-owner": "현재 소유자", + "argument-relation-query": "관련 엔티티", + "argument-type": "인수 유형", + "attribute": "속성", + "copy-argument-name": "인수 이름 복사", + "timeseries-key": "시계열 키", + "device-name": "장치 이름", + "latest-telemetry": "최근 데이터", + "rolling": "시계열 롤링", + "attribute-scope": "속성 범위", + "server-attributes": "서버 속성", + "client-attributes": "클라이언트 속성", + "shared-attributes": "공유 속성", + "attribute-key": "속성 키", + "default-value": "기본값", + "default-value-required": "기본값이 필요합니다.", + "limit": "최대 값", + "time-window": "타임윈도우", + "customer-name": "고객 이름", + "asset-name": "자산 이름", + "timeseries": "시계열", + "output": "출력", + "output-hint": "출력 처리 방법을 정의합니다.", + "create": "새 계산 필드 만들기", + "file": "계산 필드 파일", + "invalid-file-error": "잘못된 파일 형식입니다. 파일이 올바른 JSON 파일인지 확인하세요.", + "import": "계산 필드 가져오기", + "export": "계산 필드 내보내기", + "export-failed-error": "계산 필드를 내보낼 수 없습니다: {{error}}", + "output-type": "출력 유형", + "delete-title": "계산 필드 '{{title}}'을(를) 삭제하시겠습니까?", + "delete-text": "주의: 확인 후 계산 필드와 모든 관련 데이터를", + "delete-multiple-title": "{ count, plural, =1 {1개 계산 필드} other {#개 계산 필드} }를 삭제하시겠습니까?", + "delete-multiple-text": "주의: 확인 후 선택한 모든 계산 필드가 제거되고", + "test-with-this-message": "이 메시지로 테스트", + "use-latest-timestamp": "최신 타임스탬프 사용", + "entity-coordinates": "엔티티 좌표", + "latitude-time-series-key": "위도 시계열 키", + "latitude-time-series-key-required": "위도 시계열 키가 필요합니다.", + "longitude-time-series-key": "경도 시계열 키", + "longitude-time-series-key-required": "경도 시계열 키가 필요합니다.", + "geofencing-zone-groups": "지오펜싱 영역 그룹", + "geofencing-zone-groups-settings": "지오펜싱 영역 그룹 설정", + "target-zone": "대상 영역", + "perimeter-key": "경계 키", + "report-strategy": "보고 전략", + "no-zone-configured": "최소 하나의 영역이 필요합니다.", + "no-zone-configured-required": "최소 하나의 영역 그룹을 구성해야 합니다.", + "add-zone-group": "영역 그룹 추가", + "report-transition-event-only": "전환 이벤트만", + "report-presence-status-only": "존재 상태만", + "report-transition-event-and-presence": "존재 상태 및 전환 이벤트", + "perimeter-attribute-key": "경계 속성 키", + "perimeter-attribute-key-required": "경계 속성 키가 필요합니다.", + "perimeter-attribute-key-pattern": "경계 속성 키가 올바르지 않습니다.", + "entity-zone-relationship": "엔티티에서 영역으로의 경로", + "direction": "관계 방향", + "direction-from": "엔티티에서 영역으로", + "direction-to": "영역에서 엔티티로", + "relation-type": "관계 유형", + "create-relation-with-matched-zones": "일치하는 영역과 소스 엔티티의 관계 생성", + "relation-level": "관계 수준", + "fetch-last-available-level": "마지막 사용 가능한 수준만 가져오기", + "zone-group-refresh-interval": "영역 그룹 새로고침 간격", + "copy-zone-group-name": "영역 그룹 이름 복사", + "open-details-page": "엔티티 상세 페이지 열기", + "level": "수준", + "direction-level": "범례 방향", + "direction-up": "위로", + "direction-up-parent": "부모로 위로", + "direction-down": "아래로", + "direction-down-child": "자식으로 아래로", + "add-level": "수준 추가", + "delete-level": "수준 삭제", + "no-level": "구성된 수준 없음", + "levels-required": "최소 하나의 수준을 구성해야 합니다.", + "max-allowed-levels-error": "관계 수준이 최대 허용값을 초과합니다.", + "propagation-path-related-entities": "관련 엔티티로의 전파 경로", + "propagate-type": { + "arguments-only": "인수만", + "expression-result": "계산 결과" + }, + "script": "스크립트", + "data-propagate": "전파할 데이터", + "output-key": "출력 키", + "copy-output-key": "출력 키 복사", + "aggregation-path-related-entities": "관련 엔티티로의 집계 경로", + "deduplication-interval": "중복 제거 간격", + "deduplication-interval-min": "중복 제거 간격은 최소 {{ sec }}초여야 합니다.", + "deduplication-interval-hint": "텔레메트리 집계 사이의 최소 시간.", + "deduplication-interval-required": "중복 제거 간격이 필요합니다.", + "calculated-field-filter-title": "계산 필드 필터", + "filter-title": "필터", + "calculated-field-types": "계산 필드 유형", + "events": "이벤트", + "any-type": "모든 유형", + "metrics": { + "metrics": "메트릭", + "metrics-empty": "최소 하나의 메트릭을 구성해야 합니다.", + "metric-name": "메트릭 이름", + "metric-name-required": "메트릭 이름이 필요합니다.", + "metric-name-pattern": "메트릭 이름이 올바르지 않습니다.", + "metric-name-duplicate": "같은 이름의 메트릭이 이미 존재합니다.", + "metric-name-max-length": "메트릭 이름은 256자 미만이어야 합니다.", + "metric-name-forbidden": "메트릭 이름은 예약어이므로 사용할 수 없습니다.", + "copy-metric-name": "메트릭 이름 복사", + "argument-name": "인수 이름", + "aggregation": "집합", + "aggregation-type": { + "avg": "평균", + "min": "최소", + "max": "최대", + "sum": "합계", + "count": "숫자", + "count-unique": "고유 개수" + }, + "filtered": "필터됨", + "value-source": "값 소스", + "value-source-hint": "집계에 사용할 값을 얻는 방법을 정의합니다.", + "value-source-type": { + "key": "키", + "function": "함수" + }, + "no-metrics-configured": "최소 하나의 메트릭이 필요합니다.", + "add-metric": "메트릭 추가", + "max-metrics": "최대 메트릭 수에 도달했습니다.", + "metric-settings": "메트릭 설정", + "filter": "필터", + "filter-hint": "집계 중 엔티티 필터링을 활성화합니다. 필터 함수는 부울 값을 반환해야 하며 구성된 모든 인수를 사용할 수 있습니다." + }, + "output-strategy": { + "strategy": "전략", + "process-right-away": "즉시 처리", + "process-rule-chains": "규칙 사슬을 통해 처리", + "save-time-series": "시계열에 저장", + "save-database": "데이터베이스에 저장", + "save-latest-values": "최신 값에 저장", + "send-web-sockets": "WebSocket으로 전송", + "save-calculated-fields": "계산 필드로 전송", + "update-attribute-only-on-value-change": "값 변경 시에만 속성 업데이트", + "send-attributes-updated-notification": "속성 업데이트 알림 전송", + "ttl": "사용자 지정 TTL", + "ttl-required": "TTL이 필요합니다", + "ttl-min": "최소 TTL은 0만 허용됩니다", + "processing-parameters": "처리 매개변수", + "hint": { + "strategy": "결과를 즉시 처리할지 추가 처리를 위해 규칙 사슬로 전송할지 제어합니다.", + "processing-options": "처리 옵션", + "update-attribute-only-on-value-change": "값이 변경되었는지 여부에 관계없이 들어오는 모든 메시지에서 속성을 업데이트합니다. 이는 API 사용량을 증가시키고 성능을 저하시킵니다.", + "update-attribute-only-on-value-change-enabled": "값이 변경될 때만 속성을 업데이트합니다. 값이 변경되지 않으면 타임스탬프가 업데이트되지 않고 알림이 전송되지 않습니다.", + "send-attributes-updated-notification": "기본 규칙 사슬로 속성 업데이트 이벤트를 전송합니다.", + "save-time-series": "시계열 데이터를 데이터베이스의 ts_kv 테이블에 저장합니다.", + "save-database": "속성 데이터를 데이터베이스에 저장합니다.", + "save-latest-values": "새 값이 더 최근인 경우 데이터베이스의 ts_kv_latest 테이블에서 시계열 데이터를 업데이트합니다.", + "send-web-sockets-attribute": "속성 데이터 업데이트에 대해 WebSocket 구독에 알립니다.", + "send-web-sockets-time-series": "시계열 데이터 업데이트에 대해 WebSocket 구독에 알립니다.", + "save-calculated-fields-attribute": "속성 데이터 업데이트에 대해 계산 필드에 알립니다.", + "save-calculated-fields-time-series": "시계열 데이터 업데이트에 대해 계산 필드에 알립니다.", + "ttl": "시계열 데이터의 보존 기간을 정의합니다. 비활성화하면 테넌트 프로필 TTL이 사용됩니다." + } + }, + "aggregate-interval-type": "집계 간격 유형", + "aggregate-interval-value": "집계 간격 값", + "aggregate-interval-value-required": "집계 간격 값이 필요합니다.", + "aggregate-interval-value-min": "집계 간격 값은 최소 { sec, plural, =0 {0초} =1 {1초} other {#초} }여야 합니다.", + "aggregate-interval-value-step-multiple-of": "집계 간격 값은 1일의 약수 또는 배수여야 합니다.", + "aggregate-period": { + "hour": "시간", + "day": "일", + "week": "주 (월 - 일)", + "week-sun-sat": "주 (일 - 토)", + "month": "달", + "quarter": "분기", + "year": "년", + "custom": "사용자 지정" + }, + "aggregate-period-hint-offset": "집계 간격은 {{ interval }}입니다.", + "aggregate-period-hint-offset-and-so-on": "집계 간격은 {{ interval }} 등입니다.", + "entity-aggregation": { + "argument-hint": "현재 엔티티에서 데이터를 가져옵니다.", + "argument-title-hint": "집계에 사용할 입력 인수를 정의합니다.", + "argument-setting-hint": "최신 텔레메트리는 이 계산 필드에서 사용 가능한 유일한 인수 유형입니다.", + "aggregation-interval": "집계 간격", + "aggregation-interval-hint": "집계 수행 빈도를 정의합니다. 예: 1시간마다 00:00, 01:00, 02:00 등에 데이터를 집계합니다. 집계 결과는 집계 간격의 시작에 해당하는 타임스탬프와 함께 저장됩니다.", + "apply-offset": "집계 간격에 오프셋 적용", + "apply-offset-hint": "각 집계 기간의 시작을 얼마나 이동할지 정의합니다 (예: +10분 - 00:10, 01:10).", + "offset-value": "오프셋 값", + "offset-value-required": "오프셋 값이 필요합니다.", + "offset-value-min": "오프셋 값은 양의 정수여야 합니다.", + "offset-value-max": "오프셋 값은 집계 간격 값보다 작아야 합니다.", + "wait-delay": "지연된 텔레메트리에 대한 대기 시간 초과 적용", + "wait-delay-hint": "간격이 끝난 후 지연된 텔레메트리를 얼마나 기다릴지 정의합니다. 이러한 텔레메트리가 도착하면 해당 간격의 결과가 다시 계산됩니다.", + "duration": "기간", + "duration-required": "지속 시간이 필요합니다.", + "duration-min": "지속 시간은 최소 1분이어야 합니다.", + "duration-hint": "간격이 끝난 후 지연된 데이터를 얼마나 기다릴지.", + "produce-intermediate-result": "중간 결과 생성", + "produce-intermediate-result-hint": "현재 간격 동안 지표를 계산하여 중간 결과를 생성합니다." + }, + "hint": { + "arguments-simple-with-rolling": "단순 유형 계산 필드에는 시계열 롤링 유형의 키가 포함되지 않아야 합니다.", + "arguments-propagate-arguments-with-rolling": "'시계열 롤링' 유형은 '인수만' 전파와 호환되지 않습니다.", + "arguments-propagate-argument-entity-type": "엔티티 유형은 '인수만' 전파와 호환되지 않습니다.", + "arguments-propagate-argument-must-current-entity": "최소 하나의 인수가 '현재 엔티티' 소스 엔티티로 구성되어야 합니다.", + "arguments-empty": "최소 하나의 인수를 지정해야 합니다.", + "expression-required": "표현식이 필요합니다.", + "expression-invalid": "표현식이 올바르지 않습니다", + "expression-max-length": "표현식 길이는 255자 미만이어야 합니다.", + "argument-name-required": "인수 이름이 필요합니다.", + "argument-name-pattern": "인수 이름이 올바르지 않습니다.", + "argument-name-duplicate": "같은 이름의 인수가 이미 존재합니다.", + "argument-name-max-length": "인수 이름은 256자 미만이어야 합니다.", + "argument-name-forbidden": "인수 이름은 예약어이므로 사용할 수 없습니다.", + "output-key-required": "출력 키가 필요합니다.", + "output-key-pattern": "출력 키가 올바르지 않습니다.", + "output-key-duplicate": "같은 이름의 키가 이미 존재합니다.", + "output-key-max-length": "출력 키는 256자 미만이어야 합니다.", + "output-key-forbidden": "출력 키는 예약어이므로 사용할 수 없습니다.", + "entity-type-required": "엔티티 유형이 필요합니다", + "name-required": "이름을 입력하세요.", + "name-pattern": "이름이 올바르지 않습니다.", + "name-duplicate": "같은 이름의 이름이 이미 존재합니다.", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "name-forbidden": "이름은 예약어이므로 사용할 수 없습니다.", + "argument-type-required": "인수 유형이 필요합니다.", + "max-args": "최대 인수 수에 도달했습니다.", + "decimals-range": "기본 소수 자릿수는 0~15 사이의 숫자여야 합니다.", + "expression": "기본 식은 온도를 화씨에서 섭씨로 변환하는 방법을 보여줍니다.", + "arguments-entity-not-found": "인수 대상 엔티티를 찾을 수 없습니다.", + "use-latest-timestamp": "사용하면 계산된 값이 가장 최신 타임스탬프를 사용하여 저장됩니다.", + "entity-coordinates": "엔티티 GPS 좌표(위도 및 경도)를 제공하는 시계열 키를 지정하세요.", + "geofencing-zone-groups": "확인할 하나 이상의 지오펜싱 영역 그룹을 정의하세요 (예: 'allowedZones', 'restr", + "perimeter-attribute-key": "지오펜싱 영역 경계 정의가 포함된 속성 키를 설정하세요.", + "report-strategy": "현재 상태 보고서는 엔티티가 영역 내부 또는 외부에 있는지 보고합니다.", + "create-relation-with-matched-zones": "엔티티와 해당 엔티티가 포함된 영역 간의 관계를 자동으로 생성하고 유지 관리합니다.", + "relation-type-required": "관계 유형을 입력하세요.", + "relation-level-required": "관계 수준이 필요합니다.", + "relation-level-min": "최소 관계 수준 값은 1입니다.", + "relation-level-max": "최대 관계 수준 값은 {{max}}입니다.", + "geofencing-empty": "최소 하나의 영역 그룹을 구성해야 합니다.", + "geofencing-entity-not-found": "지오펜싱 대상 엔티티를 찾을 수 없습니다.", + "max-geofencing-zone": "최대 지오펜싱 영역 수에 도달했습니다.", + "zone-group-refresh-interval": "관련 엔티티를 통해 구성된 영역 그룹이 새로고침되는 빈도를 정의합니다.", + "zone-group-refresh-interval-required": "영역 그룹 새로고침 간격이 필요합니다.", + "zone-group-refresh-interval-min": "영역 그룹 새로고침 간격은 최소 {{ min }}초여야 합니다.", + "propagation-path-related-entities": "선택한 방향을 기반으로 관련 엔티티에 대한 직접적인 단일 수준 경로를 정의합니다.", + "data-propagate": "아래 구성된 인수에서 전파할 데이터를 정의합니다. '인수만'", + "aggregation-path-related-entities": "부모 또는 자식과의 직접 관계를 통한 단일 수준 집계 경로를 정의합니다.", + "arguments-aggregation": "필터링 및 집계에 사용되는 입력 인수를 정의합니다.", + "setting-arguments-aggregation": "집계 경로에 구성된 관련 엔티티에서 데이터를 가져옵니다.", + "metrics": "구성된 인수를 기반으로 집계된 지표를 정의합니다.", + "entity-aggregation-metrics": "지정된 시간 동안 구성된 인수를 기반으로 집계된 지표를 정의합니다.", + "import-invalid-calculated-field-type": "계산 필드를 가져올 수 없습니다: 잘못된 계산 필드 구조.", + "simple-expression-title": "계산된 값을 계산하는 방법을 정의하는 산술 식입니다.", + "script-title": "계산 로직과 출력 값을 정의하는 TBEL 스크립트입니다.", + "simple-arguments": "계산된 값을 계산하는 방법을 정의하는 산술 식입니다.", + "script-arguments": "스크립트에서 사용할 수 있는 입력 인수를 정의합니다." + } + }, + "alarm-rule": { + "alarm-rules-tab": "알람 규칙", + "alarm-rule": "알람 규칙", + "alarm-rules": "알람 규칙", + "alarm-rules-old": "이전", + "alarm-rules-actual": "실제", + "severities": "심각도", + "cleared": "해제 조건", + "delete-title": "알람 규칙 '{{title}}'을(를) 삭제하시겠습니까?", + "delete-text": "주의: 확인 후 알람 규칙과 모든 관련 데이터를", + "delete-multiple-title": "{ count, plural, =1 {1개 알람 규칙} other {#개 알람 규칙} }을(를) 삭제하시겠습니까?", + "delete-multiple-text": "주의: 확인 후 선택한 모든 알람 규칙이 제거되고", + "create": "새 알람 규칙 만들기", + "add": "알람 규칙 추가", + "copy": "알람 규칙 구성 복사", + "details": "알람 규칙 상세", + "no-found": "알람 규칙이 없습니다", + "list": "{ count, plural, =1 {1개 알람 규칙} other {#개 알람 규칙 목록} }", + "selected-fields": "{ count, plural, =1 {1개 알람 규칙} other {#개 알람 규칙} } 선택됨", + "import": "알람 규칙 가져오기", + "file": "알람 규칙 파일", + "export": "알람 규칙 내보내기", + "export-failed-error": "알람 규칙을 내보낼 수 없습니다: {{error}}", + "entity-type": "엔티티 유형", + "entity-type-required": "엔티티의 유형을 입력하세요.", + "alarm-type": "알람 유형", + "alarm-type-hint": "충돌을 방지하기 위해 알람 발생자(장치, 자산 등) 범위 내의 고유 식별자(예: HighTempAlarm).", + "alarm-type-required": "알람 유형은 필수 항목입니다.", + "alarm-type-pattern": "알람 유형이 올바르지 않습니다.", + "alarm-type-max-length": "알람 유형은 256자 미만이어야 합니다.", + "clear-alarm": "알람 해제", + "value-argument": "인수", + "value-argument-required": "인수가 필요합니다.", + "static-settings": "정적 설정", + "configuration": "설정", + "static-schedule": "정적", + "dynamic-schedule": "동적", + "operation-and": "그리고", + "operation-or": "또는", + "condition-during": "{{during}} 동안", + "condition-during-dynamic": "\"{{ attribute }}\" 동안", + "condition-repeat-times": "{ count, plural, =1 {1회} other {#회} } 반복", + "condition-repeat-times-dynamic": "\"{{ attribute }}\" 번 반복", + "filter-preview": "필터 미리보기", + "condition-settings": "조건 설정", + "static": "정적", + "dynamic": "동적", + "argument-filters": "인수 필터", + "argument-name": "인수 이름", + "value-type": "값 유형", + "general": "일반", + "filters": "필터", + "date-time-hint": "인수는 에포크 밀리초여야 합니다. 예: 1698839340000은 2023-11-01입니다.", + "operation": "연산", + "value-source": "값 소스", + "value": "값", + "ignore-case": "대소문자 무시", + "condition": "조건", + "script": "스크립트", + "add-filter": "인수 필터 추가", + "edit-filter": "인수 필터", + "remove-filter": "인수 필터 제거", + "no-filter": "최소 하나의 필터가 필요합니다.", + "conditions": { + "simple": "간단", + "duration": "기간", + "repeating": "반복" + }, + "schedule-title": "일정", + "edit-schedule": "알람 일정 편집", + "schedule-type": "스케줄러 유형", + "schedule-type-required": "스케줄러 유형은 필수 항목입니다.", + "schedule": { + "any-time": "항상 활성", + "specific-time": "특정 시간에 활성", + "custom": "사용자 지정" + }, + "schedule-day": { + "monday": "월요일", + "tuesday": "화요일", + "wednesday": "수요일", + "thursday": "목요일", + "friday": "금요일", + "saturday": "토요일", + "sunday": "일요일" + }, + "schedule-days": "요일", + "schedule-time": "시간", + "schedule-time-from": "부터", + "schedule-time-to": "까지", + "schedule-days-of-week-required": "최소 하나의 요일을 선택해야 합니다.", + "tbel": "TBEL", + "expression-type": { + "simple": "간단", + "script": "스크립트" + }, + "operation-type": { + "and": "그리고", + "or": "또는" + }, + "filter-predicate-type": { + "string": "문자열", + "numeric": "숫자", + "boolean": "불리언", + "complex": "복합" + }, + "alarm-rule-additional-info": "추가 정보", + "edit-alarm-rule-additional-info": "추가 정보 수정", + "alarm-rule-additional-info-placeholder": "알람 세부 정보 내에 표시할 설명과 조정 사항을 여기에 입력하세요.", + "alarm-rule-additional-info-hint": "힌트: ${인수 이름{{dashboardTitle}}는 이제 공개되어서 다음 공개 링크를 통해 접근될 수 있습니다:", "public-dashboard-notice": "참고: 관련된 장치에 접근하기 위해서는 장치들 또한 공개되어야 합니다.", "make-private-dashboard-title": "대시보드 '{{dashboardTitle}}'를 비공개로 전환하시겠습니까?", "make-private-dashboard-text": "대시보드는 비공개로 전환되어 다른 이들이 접근할 수 없게 됩니다.", "make-private-dashboard": "대시보드 비공개 전환", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-text": "'{{dashboardTitle}}' - ThingsBoard 제공", + "socialshare-title": "'{{dashboardTitle}}' - ThingsBoard 제공", "select-dashboard": "대시보드 선택", "no-dashboards-matching": "'{{entity}}'와 일치하는 대시보드가 없습니다.", "dashboard-required": "대시보드를 입력하세요.", @@ -682,12 +1892,26 @@ "background-image": "배경 이미지", "background-size-mode": "배경 사이즈 모드", "no-image": "이미지 없음", + "empty-image": "이미지 없음", "drop-image": "이곳에 이미지를 끌어다놓거나 이곳을 클릭하여 파일을 선택하고 업로드하세요.", + "maximum-upload-file-size": "최대 업로드 파일 크기: {{ size }}", + "cannot-upload-file": "파일을 업로드할 수 없습니다", "settings": "설정", + "move-all-widgets": "모든 위젯 이동", + "move-by": "이동 거리", + "cols": "열", + "rows": "행", + "layout": "레이아웃", + "layout-type-default": "기본값", + "layout-type-scada": "SCADA", + "layout-type-divider": "구분선", + "layout-settings-type": "레이아웃 설정: {{ type }} 브레이크포인트", "columns-count": "열 개수", "columns-count-required": "열 개수를 입력하세요.", "min-columns-count-message": "열 개수를 최소 10 이상으로 입력하세요.", "max-columns-count-message": "열 개수를 최대 100 이하로 입력하세요.", + "min-layout-width": "최소 레이아웃 너비", + "columns-suffix": "열", "widgets-margins": "위젯 사이 여백 크기", "margin-required": "여백 값을 입력하세요.", "min-margin-message": "최소 여백 값은 0보다 커야 합니다.", @@ -700,23 +1924,39 @@ "vertical-margin-required": "가로 여백 값을 입력하세요.", "min-vertical-margin-message": "최소 가로 여백 값은 0보다 커야 합니다.", "max-vertical-margin-message": "최대 가로 여백 값은 50보다 작아야 합니다.", + "apply-outer-margin": "레이아웃 측면에 여백 적용", "autofill-height": "레이아웃 세로 길이 자동 채우기", "mobile-layout": "모바일 레이아웃 설정", "mobile-row-height": "모바일 행 높이, px", "mobile-row-height-required": "모바일 행 높이를 입력하세요.", "min-mobile-row-height-message": "최소 모바일 행 높이 값은 5 픽셀보다 커야 합니다.", "max-mobile-row-height-message": "최대 모바일 행 높이 값은 200 픽셀보다 작아야 합니다.", + "row-height": "행 높이", + "row-height-required": "행 높이 값은 필수 항목입니다.", + "min-row-height-message": "최소 행 높이 값은 5픽셀만 허용됩니다.", + "max-row-height-message": "최대 행 높이 값은 200픽셀만 허용됩니다.", + "display-first-in-mobile-view": "모바일 보기에서 먼저 표시", + "title-settings": "제목 설정", "display-title": "대시보드 제목 표시", - "toolbar-always-open": "도구 상자를 열어두기", "title-color": "제목 색상", + "toolbar-settings": "도구 모음 설정", + "hide-toolbar": "도구 모음 숨기기", + "toolbar-always-open": "도구 상자를 열어두기", "display-dashboards-selection": "대시보드 선택 표시", - "display-entities-selection": "개체 선택 표시", + "display-entities-selection": "엔티티 선택 표시", "display-filters": "필터 표시", "display-dashboard-timewindow": "타임윈도우 표시", "display-dashboard-export": "디스플레이 내보내기", + "display-update-dashboard-image": "대시보드 이미지 업데이트 표시", + "dashboard-logo-settings": "대시보드 로고 설정", + "display-dashboard-logo": "대시보드 전체 화면 모드에서 로고 표시", + "dashboard-logo-image": "대시보드 로고 이미지", + "advanced-settings": "고급 설정", + "dashboard-css": "대시보드 CSS", "import": "대시보드 가져오기", "export": "대시보드 내보내기", "export-failed-error": "대시보드 내보내기를 할 수 없습니다.: {error}", + "export-prompt": "대시보드 이미지 및 리소스 포함", "create-new-dashboard": "대시보드 생성", "dashboard-file": "대시보드 파일", "invalid-dashboard-file-error": "대시보드 가져오기를 할 수 없습니다.: 대시보드 데이터 구조가 잘못되었습니다.", @@ -732,14 +1972,17 @@ "alias-resolution-error-title": "대시보드 별명 구성 오류", "invalid-aliases-config": "일부 별명 필터와 일치하는 장치를 찾을 수 없습니다.
이 문제를 해결하려면 관리자에게 문의하십시오.", "select-devices": "장치 선택", - "assignedToCustomer": "할당된 커스터머", - "assignedToCustomers": "할당된 커스터머", + "assignedToCustomer": "할당된 고객", + "assignedToCustomers": "할당된 고객", "public": "공개", + "copyId": "대시보드 ID 복사", + "idCopiedMessage": "대시보드 ID가 클립보드에 복사되었습니다", "public-link": "공개 링크", "copy-public-link": "공개 링크 복사", "public-link-copied-message": "대시보드 공개 링크가 클립보드로 복사되었습니다", "manage-states": "대시보드 상태 관리", "states": "대시보드 상태", + "states-short": "상태", "search-states": "대시보드 상태 검색", "selected-states": "{ count, plural, =1 {1 개 대시보드 상태} other {# 개 대시보드 상태} } 선택됨", "edit-state": "대시보드 상태 편집", @@ -759,12 +2002,38 @@ "hide-details": "상세 내역 숨기기", "select-state": "대상 상태 선택", "state-controller": "상태 컨트롤러", + "state-controller-default": "정적 (사용 중단)", "search": "대시보드 검색", - "selected-dashboards": "{ count, plural, =1 {1 개 대시보드} other {# 개 대시보드} } 선택됨" + "selected-dashboards": "{ count, plural, =1 {1 개 대시보드} other {# 개 대시보드} } 선택됨", + "home-dashboard": "홈 대시보드", + "home-dashboard-hide-toolbar": "홈 대시보드 도구 모음 숨기기", + "unassign-dashboard-from-edge-text": "확인 후 대시보드 할당이 해제되고", + "unassign-dashboards-from-edge-title": "{ count, plural, =1 {1 개 대시보드} other {# 개 대시보드} }의 할당을 취소하시겠습니까?", + "unassign-dashboards-from-edge-text": "확인 후 선택한 모든 대시보드 할당이 해제되고", + "assign-dashboard-to-edge": "엣지에 대시보드 할당", + "assign-dashboard-to-edge-text": "엣지에 할당할 대시보드를 선택하세요", + "non-existent-dashboard-state-error": "ID가 \"{{ stateId }}\"인 대시보드 상태를 찾을 수 없습니다", + "edit-mode": "수정 모드", + "duplicate-state-action": "상태 복제", + "breakpoint-value": "브레이크포인트 ({{ value }})", + "breakpoints-id": { + "default": "기본값", + "xs": "모바일 (xs)", + "sm": "태블릿 (sm)", + "md": "노트북 (md)", + "lg": "데스크톱 (lg)", + "xl": "데스크톱 (xl)" + }, + "view-format-type-grid": "그리드", + "view-format-type-list": "목록", + "view-format": "보기 형식" }, "datakey": { "settings": "설정", + "general": "일반", "advanced": "고급", + "key": "키", + "keys": "키", "label": "라벨", "color": "색상", "units": "값 뒤에 표기할 특수 기호", @@ -774,25 +2043,64 @@ "configuration": "데이터 키 구성", "timeseries": "시계열", "attributes": "속성", - "entity-field": "개체 필드", + "entity-field": "엔티티 필드", "alarm": "알람 필드", "timeseries-required": "장치 시계열 를 입력하세요.", "timeseries-or-attributes-required": "장치 시계열/속성 를 입력하세요.", - "alarm-fields-timeseries-or-attributes-required": "Alarm fields or entity timeseries/attributes are required.", - "maximum-timeseries-or-attributes": "Maximum { count, plural, =1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", + "alarm-fields-timeseries-or-attributes-required": "알람 필드 또는 엔티티 시계열/속성이 필요합니다.", + "maximum-timeseries-or-attributes": "최대 { count, plural, =1 {1개의 시계열/속성이 허용됩니다.} other {#개의 시계열/속성이 허용됩니다.} }", "alarm-fields-required": "알람 필드를 입력하세요.", "function-types": "함수 유형", + "function-type": "함수 유형", "function-types-required": "함수 유형을 입력하세요.", + "data-keys": "데이터 키", + "data-key": "데이터 키", + "data-keys-required": "데이터 키는 필수 항목입니다.", + "data-key-required": "데이터 키는 필수 항목입니다.", + "alarm-keys": "알람 데이터 키", + "alarm-key": "알람 데이터 키", + "alarm-key-functions": "알람 키 함수", + "alarm-key-function": "알람 키 함수", + "latest-keys": "최신 데이터 키", + "latest-key": "최신 데이터 키", + "latest-key-functions": "최신 키 함수", + "latest-key-function": "최신 키 함수", + "timeseries-keys": "시계열 데이터 키", + "timeseries-key": "시계열 데이터 키", + "timeseries-key-functions": "시계열 키 함수", + "timeseries-key-function": "시계열 키 함수", "maximum-function-types": "최대 { count, plural, =1 {1 개 함수} other {# 개 함수} } 종류만 허용됩니다.", "time-description": "현재 값의 타임스탬프;", "value-description": "현재 값;", "prev-value-description": "이전 함수 호출의 결과;", "time-prev-description": "이전 값의 타임스탬프;", - "prev-orig-value-description": "원래 이전 값;" + "prev-orig-value-description": "원래 이전 값;", + "aggregation": "집합", + "aggregation-type-hint-common": "성능상의 이유로 집계 값 계산은 다음에만 사용할 수 있습니다", + "aggregation-type-none-hint": "최신 값을 가져옵니다.", + "aggregation-type-min-hint": "선택한 시간 창 내의 데이터 포인트 중 최소값을 찾습니다.", + "aggregation-type-max-hint": "선택한 시간 창 내의 데이터 포인트 중 최대값을 찾습니다.", + "aggregation-type-avg-hint": "선택한 시간 창 내의 데이터 포인트 평균을 계산합니다.", + "aggregation-type-sum-hint": "선택한 시간 창 내의 데이터 포인트의 모든 값을 합산합니다.", + "aggregation-type-count-hint": "선택한 시간 창 내의 데이터 포인트 총 수입니다.", + "delta-calculation": "델타 계산", + "enable-delta-calculation": "델타 계산 사용", + "enable-delta-calculation-hint": "사용하면 데이터 키 값이 집계된 값을 기반으로 계산됩니다.", + "delta-calculation-result": "델타 계산 결과", + "delta-calculation-result-previous-value": "이전 값", + "delta-calculation-result-delta-absolute": "델타 (절대값)", + "delta-calculation-result-delta-percent": "델타 (퍼센트)", + "source": "소스", + "latest": "최신", + "latest-value": "최신 값", + "delta": "델타", + "percent": "퍼센트", + "absolute": "절대값" }, "datasource": { "type": "데이터소스 유형", "name": "이름", + "label": "라벨", "add-datasource-prompt": "데이터소스를 추가하세요." }, "details": { @@ -808,6 +2116,7 @@ "management": "장치 관리", "view-devices": "장치 보기", "device-alias": "장치 별명", + "device-type-max-length": "장치 유형은 256자 미만이어야 합니다.", "aliases": "장치 별명", "no-alias-matching": "'{{alias}}' 를 찾을 수 없습니다.", "no-aliases-found": "별명이 없습니다.", @@ -823,51 +2132,101 @@ "remove-alias": "장치 별명 삭제", "add-alias": "장치 별명 추가", "name-starts-with": "시작되는 이름", + "help-text": "필요에 따라 '%'를 사용하세요: '%device_name_contains%', '%device_name_ends', 'devic", "device-list": "장치 리스트", "use-device-name-filter": "필터 사용", "device-list-empty": "선택된 장치가 없습니다.", "device-name-filter-required": "장치 필터 이름을 입력하세요.", "device-name-filter-no-device-matched": "'{{device}}' 로 시작되는 장치를 찾을 수 없습니다.", "add": "장치 추가", - "assign-to-customer": "커스터머에게 할당", - "assign-device-to-customer": "장치를 커스터머에게 할당", - "assign-device-to-customer-text": "커스터머에게 할당할 장치를 선택하십시오.", + "assign-to-customer": "고객에게 할당", + "assign-device-to-customer": "장치를 고객에게 할당", + "assign-device-to-customer-text": "고객에게 할당할 장치를 선택하십시오.", + "assign-device-to-edge-title": "엣지에 장치 할당", + "assign-device-to-edge-text": "엣지에 할당할 장치를 선택하세요", "make-public": "장치를 공개로 전환", "make-private": "장치를 비공개로 전환", "no-devices-text": "장치 없음", - "assign-to-customer-text": "장치에 할당할 커스터머를 선택하세요.", + "assign-to-customer-text": "장치에 할당할 고객을 선택하세요.", "device-details": "장치 상세 정보", "add-device-text": "장치 추가", "credentials": "자격 증명", "manage-credentials": "자격 증명 관리", "delete": "장치 삭제", "assign-devices": "장치 할당", - "assign-devices-text": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }를 커스터머에 할당", + "assign-devices-text": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }를 고객에 할당", "delete-devices": "장치 삭제", - "unassign-from-customer": "커스터머 할당 해제", + "unassign-from-customer": "고객 할당 해제", "unassign-devices": "장치 할당 취소", - "unassign-devices-action-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }를 커스터머에게서 할당 해제", + "unassign-devices-action-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }를 고객에게서 할당 해제", + "unassign-device-from-edge-title": "'{{deviceName}}' 장치 할당을 해제하시겠습니까?", + "unassign-device-from-edge-text": "확인 후 장치 할당이 해제되고", + "unassign-devices-from-edge": "엣지에서 장치 할당 해제", "assign-new-device": "새로운 장치 할당", - "make-public-device-title": "Are you sure you want to make the device '{{deviceName}}' public?", - "make-public-device-text": "After the confirmation the device and all its data will be made public and accessible by others.", - "make-private-device-title": "Are you sure you want to make the device '{{deviceName}}' private?", - "make-private-device-text": "After the confirmation the device and all its data will be made private and won't be accessible by others.", - "view-credentials": "크리덴셜 보기", + "make-public-device-title": "장치 '{{deviceName}}'을(를) 공개하시겠습니까?", + "make-public-device-text": "확인 후 장치와 모든 데이터가 공개되어 다른 사용자가 액세스할 수 있습니다.", + "make-private-device-title": "장치 '{{deviceName}}'을(를) 비공개로 만드시겠습니까?", + "make-private-device-text": "확인 후 장치와 모든 데이터가 비공개되어 다른 사용자가 액세스할 수 없습니다.", + "view-credentials": "자격 증명 보기", "delete-device-title": "'{{deviceName}}' 장치를 삭제하시겠습니까?", "delete-device-text": "장치 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "delete-devices-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }를 삭제하시겠습니까?", "delete-devices-action-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} } 삭제", "delete-devices-text": "장치 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "unassign-device-title": "'{{deviceName}}' 장치 할당을 해제하시겠습니까?", - "unassign-device-text": "장치가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", + "unassign-device-text": "장치가 할당 해제되고 고객은 액세스 할 수 없게됩니다.", "unassign-device": "장치 할당 취소", "unassign-devices-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }의 할당을 해제하시겠습니까??", - "unassign-devices-text": "선택된 장치가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.", - "device-credentials": "장치 크리덴셜", - "credentials-type": "크리덴셜 타입", - "access-token": "억세스 토큰", + "unassign-devices-text": "선택된 장치가 할당 해제되고 고객은 액세스 할 수 없게됩니다.", + "device-credentials": "장치 자격 증명", + "loading-device-credentials": "장치 자격 증명 로딩 중...", + "credentials-type": "자격 증명 타입", + "access-token": "액세스 토큰", "access-token-required": "액세스 토큰을 입력하세요.", "access-token-invalid": "액세스 토큰 길이는 1 - 32 자 여야합니다.", + "certificate-pem-format": "PEM 형식의 인증서", + "certificate-pem-format-required": "인증서는 필수 항목입니다.", + "copy-access-token": "액세스 토큰 복사", + "copy-certificate": "인증서 복사", + "copy-client-id": "클라이언트 ID 복사", + "copy-user-name": "사용자 이름 복사", + "copy-password": "비밀번호 복사", + "generate-client-id": "클라이언트 ID 생성", + "generate-user-name": "사용자 이름 생성", + "generate-password": "비밀번호 생성", + "generate-access-token": "액세스 토큰 생성", + "lwm2m-security-config": { + "identity": "클라이언트 ID", + "identity-required": "클라이언트 ID는 필수 항목입니다.", + "identity-tooltip": "PSK 식별자는 [RFC4279]에 설명된 대로 최대 128바이트의 임의 PSK 식별자입니다.", + "client-key": "클라이언트 키", + "client-key-required": "클라이언트 키는 필수 항목입니다.", + "client-key-tooltip-prk": "RPK 공개 키 또는 ID는 표준 [RFC7250]에 있어야 하며 Base64로 인코딩되어야 합니다.", + "client-key-tooltip-psk": "PSK 키는 표준 [RFC4279]에 있어야 하며 HexDec 형식이어야 합니다: 32, 64, 128자", + "endpoint": "엔드포인트 클라이언트 이름", + "endpoint-required": "엔드포인트 클라이언트 이름은 필수 항목입니다.", + "client-public-key": "클라이언트 공개 키", + "client-public-key-hint": "클라이언트 공개 키가 비어 있으면 신뢰할 수 있는 인증서가 사용됩니다.", + "client-public-key-tooltip": "X509 공개 키는 DER 인코딩된 X509v3 형식이어야 하며 EC만 지원합니다.", + "mode": "보안 구성 모드", + "client-tab": "클라이언트 보안 구성", + "client-certificate": "클라이언트 인증서", + "bootstrap-tab": "부트스트랩 클라이언트", + "bootstrap-server": "부트스트랩 서버", + "lwm2m-server": "LwM2M 서버", + "client-reboot": "등록 업데이트 트리거", + "bootstrap-reboot": "부트스트랩 요청 트리거", + "client-publicKey-or-id": "클라이언트 공개 키 또는 ID", + "client-publicKey-or-id-required": "클라이언트 공개 키 또는 ID는 필수 항목입니다.", + "client-publicKey-or-id-tooltip-psk": "PSK 식별자는 [RFC4279]에 설명된 대로 최대 128바이트의 임의 PSK 식별자입니다.", + "client-publicKey-or-id-tooltip-rpk": "RPK 공개 키 또는 ID는 표준 [RFC7250]에 있어야 하며 Base64로 인코딩되어야 합니다.", + "client-publicKey-or-id-tooltip-x509": "X509 공개 키는 DER 인코딩된 X509v3 형식이어야 하며 EC만 지원합니다.", + "client-secret-key": "클라이언트 비밀 키", + "client-secret-key-required": "클라이언트 비밀 키는 필수 항목입니다.", + "client-secret-key-tooltip-psk": "PSK 키는 표준 [RFC4279]에 있어야 하며 HexDec 형식이어야 합니다: 32, 64, 128자", + "client-secret-key-tooltip-prk": "RPK 비밀 키는 PKCS_8 형식(DER 인코딩, 표준 [RFC5958])이어야 합니다.", + "client-secret-key-tooltip-x509": "X509 비밀 키는 PKCS_8 형식(DER 인코딩, 표준 [RFC5958])이어야 합니다." + }, "client-id": "클라이언트 ID", "client-id-pattern": "잘못된 문자가 포함되어 있습니다.", "user-name": "사용자 이름", @@ -883,23 +2242,34 @@ "any-device": "모든 장치", "no-device-types-matching": "'{{entitySubtype}}'에 해당되는 장치 유형을 찾을 수 없습니다.", "device-type-list-empty": "아무 장치 유형도 선택되지 않았습니다.", + "device-profile-type-list-empty": "최소 하나의 장치 프로필을 선택해야 합니다.", "device-types": "장치 유형", "name": "이름", "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "label-max-length": "레이블은 256자 미만이어야 합니다.", "description": "설명", "label": "라벨", "events": "이벤트", "details": "상세", "copyId": "장치 아이디 복사", "copyAccessToken": "액세스 토큰 복사", - "copy-mqtt-authentication": "MQTT 크리덴셜 복사", + "copy-mqtt-authentication": "MQTT 자격 증명 복사", "idCopiedMessage": "장치 아이디가 클립보드에 복사되었습니다.", - "accessTokenCopiedMessage": "장치 억세스 토큰이 클립보드에 복사되었습니다.", - "mqtt-authentication-copied-message": "장치 MQTT 크리덴셜이 클립보드에 복사되었습니다.", - "assignedToCustomer": "커스터머에 할당됨", + "accessTokenCopiedMessage": "장치 액세스 토큰이 클립보드에 복사되었습니다.", + "mqtt-authentication-copied-message": "장치 MQTT 자격 증명이 클립보드에 복사되었습니다.", + "assignedToCustomer": "고객에 할당됨", "unable-delete-device-alias-title": "장치 별명을 삭제할 수 없습니다.", "unable-delete-device-alias-text": "'{{deviceAlias}}' 장치 별명을 삭제할 수 없습니다. 다음 위젯에서 사용하고 있습니다.
{{widgetsList}}", "is-gateway": "게이트웨이 여부", + "overwrite-activity-time": "연결된 장치의 활동 시간 덮어쓰기", + "device-filter-title": "장치 필터", + "filter-title": "필터", + "device-state": "장치 상태", + "state": "상태", + "any": "전체", + "active": "활성", + "inactive": "비활성", "public": "공개", "device-public": "장치가 공개됨", "select-device": "장치 선택", @@ -911,161 +2281,739 @@ "transport-configuration": "전송 설정", "wizard": { "device-details": "장치 상세 정보" + }, + "unassign-devices-from-edge-title": "{ count, plural, =1 {장치 1 개} other {장치 # 개} }의 할당을 해제하시겠습니까??", + "unassign-devices-from-edge-text": "확인 후 선택한 모든 장치의 할당이 해제되고", + "time": "시간", + "connectivity": { + "check-connectivity": "연결 확인", + "device-created-check-connectivity": "장치가 생성되었습니다. 연결을 확인하세요!", + "loading-check-connectivity-command": "연결 확인 명령 로딩 중...", + "use-following-instructions": "다음 지침을 사용하여 장치 대신 텔레메트리를 전송하세요.", + "execute-following-command": "다음 명령을 실행하세요", + "install-curl-windows": "Windows 10 b17063부터 cURL이 기본적으로 사용할 수 있습니다.", + "install-curl-macos": "Mac OS X 10.2 6C115 (Jaguar)부터 cURL이 기본적으로 사용할 수 있습니다.", + "install-mqtt-windows": "mosquitto_pub을 다운로드, 설치, 설정 및 실행하는 지침을 사용하세요.", + "install-coap-client": "coap-client를 다운로드, 설치, 설정 및 실행하는 지침을 사용하세요.", + "install-necessary-client-tools": "필요한 클라이언트 도구 설치", + "mqtts-x509-command": "다음 문서를 사용하여 인증서로 MQTT를 통해 장치를 연결하세요.", + "coaps-x509-command": "다음 문서를 사용하여 인증서로 CoAP over DTLS를 통해 장치를 연결하세요.", + "snmp-command": "다음 문서를 사용하여 SNMP를 통해 장치를 연결하세요.", + "sparkplug-command": "다음 문서를 사용하여 MQTT Sparkplug를 통해 장치를 연결하세요.", + "lwm2m-command": "다음 문서를 사용하여 LWM2M을 통해 장치를 연결하세요." } }, + "dynamic-form": { + "property": { + "properties": "속성", + "property": "속성", + "id": "ID", + "name": "이름", + "type": "유형", + "type-text": "텍스트", + "type-password": "비밀번호", + "type-textarea": "텍스트 영역", + "type-number": "숫자", + "type-switch": "스위치", + "type-select": "선택", + "type-radios": "라디오 버튼", + "type-datetime": "날짜/시간", + "type-image": "이미지", + "type-javascript": "JavaScript", + "type-json": "JSON", + "type-html": "HTML", + "type-css": "CSS", + "type-markdown": "마크다운", + "type-color": "색상", + "type-color-settings": "색상 설정", + "type-font": "글꼴", + "type-units": "단위", + "type-icon": "아이콘", + "type-fieldset": "필드셋", + "type-array": "배열", + "type-html-section": "HTML 섹션", + "group-title": "그룹 제목", + "no-properties": "구성된 속성 없음", + "add-property": "속성 추가", + "property-settings": "속성 설정", + "remove-property": "속성 제거", + "default-value": "기본값", + "value-required": "값 필요", + "number-settings": "숫자 설정", + "min": "최소", + "max": "최대", + "step": "단계", + "selected-options-limit": "선택 옵션 제한", + "advanced-ui-settings": "고급 UI 설정", + "disable-on-property": "속성 비활성화", + "disable-on-property-none": "없음 (필드 항상 활성화)", + "display-condition-function": "표시 조건 함수", + "sub-label": "보조 라벨", + "vertical-divider-after": "다음에 수직 구분선", + "input-field-suffix": "입력 필드 접미사", + "property-row-classes": "속성 행 클래스", + "property-field-classes": "속성 필드 클래스", + "not-unique-property-ids-error": "속성 ID는 고유해야 합니다!", + "enable-multiple-select": "다중 선택 활성화", + "allow-empty-select-option": "빈 옵션 허용", + "select-options": "선택 옵션", + "not-unique-select-option-value-error": "선택 옵션 값은 고유해야 합니다!", + "value": "값", + "label": "라벨", + "add-option": "옵션 추가", + "no-options": "구성된 옵션 없음", + "remove-option": "옵션 제거", + "textarea-rows": "텍스트 영역 행 수", + "help-id": "도움말 ID", + "buttons-direction": "버튼 방향", + "direction-row": "행", + "direction-column": "열", + "radio-button-options": "라디오 버튼 옵션", + "datetime-type": "날짜/시간 필드 유형", + "datetime-type-date": "날짜", + "datetime-type-time": "시간", + "datetime-type-datetime": "날짜/시간", + "enable-clear-button": "지우기 버튼 활성화", + "html-section-settings": "HTML 섹션 설정", + "html-section-classes": "HTML 섹션 클래스", + "html-section-content": "HTML 섹션 내용", + "array-item": "배열 항목", + "item-type": "항목 유형", + "item-name": "항목 이름", + "no-items": "항목 없음", + "support-unit-conversion": "단위 변환 지원" + }, + "clear-form": "양식 지우기", + "clear-form-prompt": "모든 양식 속성을 제거하시겠습니까?", + "import-form": "JSON에서 양식 가져오기", + "export-form": "양식을 JSON으로 내보내기", + "json-file": "JSON 파일", + "json-content": "JSON 내용", + "invalid-form-json-file-error": "JSON에서 양식을 가져올 수 없습니다: 잘못된 양식 JSON 데이터 구조입니다." + }, + "asset-profile": { + "asset-profile": "자산 프로필", + "asset-profiles": "자산 프로필", + "all-asset-profiles": "모두", + "add": "자산 프로필 추가", + "edit": "자산 프로필 수정", + "asset-profile-details": "자산 프로필 상세", + "no-asset-profiles-text": "자산 프로필이 없습니다", + "search": "자산 프로필 검색", + "selected-asset-profiles": "{ count, plural, =1 {1개 자산 프로필} other {#개 자산 프로필} } 선택됨", + "no-asset-profiles-matching": "'{{entity}}'와(과) 일치하는 자산 프로필이 없습니다.", + "asset-profile-required": "자산 프로필이 필요합니다", + "idCopiedMessage": "자산 프로필 ID가 클립보드에 복사되었습니다", + "set-default": "자산 프로필 기본값으로 설정", + "delete": "자산 프로필 삭제", + "copyId": "자산 프로필 ID 복사", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "new-device-profile-name": "자산 프로필 이름", + "new-device-profile-name-required": "자산 프로필 이름이 필요합니다.", + "name": "이름", + "name-required": "이름을 입력하세요.", + "image": "자산 프로필 이미지", + "description": "설명", + "default": "기본값", + "default-rule-chain": "기본 규칙 사슬", + "default-edge-rule-chain": "기본 엣지 규칙 사슬", + "default-edge-rule-chain-hint": "이 자산 프로필의 자산에 대한 들어오는 데이터를 처리하는 규칙 사슬로 엣지에서 사용됩니다", + "mobile-dashboard": "모바일 대시보드", + "mobile-dashboard-hint": "모바일 애플리케이션에서 자산 상세 대시보드로 사용됩니다", + "select-queue-hint": "드롭다운 목록에서 선택하세요.", + "delete-asset-profile-title": "자산 프로필 '{{assetProfileName}}'을(를) 삭제하시겠습니까?", + "delete-asset-profile-text": "주의: 확인 후 자산 프로필과 모든 관련 데이터가", + "delete-asset-profiles-title": "{ count, plural, =1 {1개 자산 프로필} other {#개 자산 프로필} }을(를) 삭제하시겠습니까?", + "delete-asset-profiles-text": "주의: 확인 후 선택한 모든 자산 프로필이 제거되고", + "set-default-asset-profile-title": "자산 프로필 '{{assetProfileName}}'을(를) 기본값으로 설정하시겠습니까?", + "set-default-asset-profile-text": "확인 후 자산 프로필이 기본값으로 표시되고", + "no-asset-profiles-found": "자산 프로필이 없습니다.", + "create-new-asset-profile": "새로 만들기!", + "create-asset-profile": "새 자산 프로필 만들기", + "import": "자산 프로필 가져오기", + "export": "자산 프로필 내보내기", + "export-failed-error": "자산 프로필을 내보낼 수 없습니다: {{error}}", + "asset-profile-file": "자산 프로필 파일", + "invalid-asset-profile-file-error": "자산 프로필을 가져올 수 없습니다: 잘못된 자산 프로필 데이터 구조입니다." + }, "device-profile": { - "device-profile": "장치 프로파일", - "device-profiles": "장치 프로파일", + "device-profile": "장치 프로필", + "device-profiles": "장치 프로필", "all-device-profiles": "모두", - "add": "장치 프로파일 추가", - "edit": "장치 프로파일 편집", - "device-profile-details": "장치 프로파일 상세 정보", - "no-device-profiles-text": "아무 장치 프로파일도 찾을 수 없습니다", - "search": "장치 프로파일 검색", - "selected-device-profiles": "{ count, plural, =1 {1 개 장치 프로파일} other {# 개 장치 프로파일} } 선택됨", - "no-device-profiles-matching": "No device profile matching '{{entity}}' were found.", - "device-profile-required": "장치 프로파일을 입력하세요", - "idCopiedMessage": "Device profile Id has been copied to clipboard", - "set-default": "Make device profile default", - "delete": "Delete device profile", - "copyId": "Copy device profile Id", + "add": "장치 프로필 추가", + "edit": "장치 프로필 편집", + "device-profile-details": "장치 프로필 상세 정보", + "no-device-profiles-text": "아무 장치 프로필도 찾을 수 없습니다", + "search": "장치 프로필 검색", + "selected-device-profiles": "{ count, plural, =1 {1 개 장치 프로필} other {# 개 장치 프로필} } 선택됨", + "no-device-profiles-matching": "'{{entity}}'와(과) 일치하는 장치 프로필을 찾을 수 없습니다.", + "device-profile-required": "장치 프로필을 입력하세요", + "idCopiedMessage": "장치 프로필 ID가 클립보드에 복사되었습니다.", + "set-default": "장치 프로필 기본값으로 설정", + "delete": "장치 프로필 삭제", + "copyId": "장치 프로필 ID 복사", + "name-max-length": "이름은 256자 미만이어야 합니다.", "name": "이름", "name-required": "이름을 입력하세요.", - "type": "프로파일 유형", - "type-required": "프로파일 유형을 입력하세요.", + "type": "프로필 유형", + "type-required": "프로필 유형을 입력하세요.", "type-default": "기본값", + "image": "장치 프로필 이미지", "transport-type": "전송 유형", "transport-type-required": "전송 유형을 입력하세요.", "transport-type-default": "기본값", "transport-type-default-hint": "기본 MQTT, HTTP, CoAP 전송을 지원", "transport-type-mqtt": "MQTT", "transport-type-mqtt-hint": "고급 MQTT 전송 설정을 활성화", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "고급 CoAP 전송 설정을 활성화합니다.", "transport-type-lwm2m": "LWM2M", "transport-type-lwm2m-hint": "LWM2M 전송 유형", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "SNMP 전송 구성을 지정하세요.", + "transport-type-http": "HTTP", "description": "설명", "default": "기본값", - "profile-configuration": "프로파일 설정", + "profile-configuration": "프로필 설정", "transport-configuration": "전송 설정", "default-rule-chain": "기본 규칙 사슬", - "select-queue-hint": "Select from a drop-down list.", - "delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?", - "delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.", - "delete-device-profiles-title": "Are you sure you want to delete { count, plural, =1 {1 개 장치 프로파일} other {# 개 장치 프로파일} }?", - "delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data will become unrecoverable.", - "set-default-device-profile-title": "Are you sure you want to make the device profile '{{deviceProfileName}}' default?", - "set-default-device-profile-text": "After the confirmation the device profile will be marked as default and will be used for new devices with no profile specified.", - "no-device-profiles-found": "No device profiles found.", - "create-new-device-profile": "Create a new one!", - "mqtt-device-topic-filters": "MQTT device topic filters", - "mqtt-device-topic-filters-unique": "MQTT device topic filters need to be unique.", - "mqtt-device-payload-type": "MQTT device payload", + "default-edge-rule-chain": "기본 엣지 규칙 사슬", + "default-edge-rule-chain-hint": "엣지에서 이 장치 프로필의 장치에 대한 들어오는 데이터를 처리하는 규칙 체인으로 사용됩니다.", + "mobile-dashboard": "모바일 대시보드", + "mobile-dashboard-hint": "모바일 애플리케이션에서 장치 세부 정보 대시보드로 사용됩니다.", + "select-queue-hint": "드롭다운 목록에서 선택하세요.", + "delete-device-profile-title": "장치 프로필 '{{deviceProfileName}}'을(를) 삭제하시겠습니까?", + "delete-device-profile-text": "주의: 확인 후 장치 프로필과 관련된 모든 데이터(OTA 업데이트 포함)는 복구할 수 없게 됩니다.", + "delete-device-profiles-title": "정말 { count, plural, =1 {1개의 장치 프로필} other {#개의 장치 프로필} }을 삭제하시겠습니까?", + "delete-device-profiles-text": "주의: 확인 후 선택한 모든 장치 프로필이 제거되고 관련된 모든 데이터(OTA 업데이트 포함)는 복구할 수 없게 됩니다.", + "set-default-device-profile-title": "장치 프로필 '{{deviceProfileName}}'을(를) 기본값으로 설정하시겠습니까?", + "set-default-device-profile-text": "확인 후 장치 프로필이 기본값으로 표시되며 프로필이 지정되지 않은 새 장치에 사용됩니다.", + "no-device-profiles-found": "장치 프로필을 찾을 수 없습니다.", + "create-new-device-profile": "새로 만들기!", + "mqtt-device-topic-filters": "MQTT 장치 토픽 필터", + "mqtt-device-topic-filters-unique": "MQTT 장치 토픽 필터는 고유해야 합니다.", + "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge of Network (EoN) 노드.", + "mqtt-device-topic-filters-spark-plug-hint": "Sparkplug B 페이로드 및 토픽 형식의 EoN 노드에서 연결을 허용합니다.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "속성으로 저장할 SparkPlug 지표.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "장치 속성으로 저장될 SparkPlug 지표의 이름입니다. 다른 모든 지표는", + "mqtt-device-payload-type": "MQTT 장치 페이로드", "mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-proto": "Protobuf", - "mqtt-payload-type-required": "Payload type is required.", - "support-level-wildcards": "Single [+] and multi-level [#] wildcards supported.", - "telemetry-topic-filter": "Telemetry topic filter", - "telemetry-topic-filter-required": "Telemetry topic filter is required.", - "attributes-topic-filter": "Attributes publish topic filter", - "attributes-subscribe-topic-filter": "Attributes subscribe topic filter", - "attributes-topic-filter-required": "Attributes publish topic filter is required.", - "attributes-subscribe-topic-filter-required": "Attributes subscribe topic is required", - "telemetry-proto-schema": "Telemetry proto schema", - "telemetry-proto-schema-required": "Telemetry proto schema is required.", - "attributes-proto-schema": "Attributes proto schema", - "attributes-proto-schema-required": "Attributes proto schema is required.", - "rpc-response-topic-filter": "RPC response topic filter", - "rpc-response-topic-filter-required": "RPC response topic filter is required.", - "not-valid-pattern-topic-filter": "Not valid pattern topic filter", - "not-valid-single-character": "Invalid use of a single-level wildcard character", - "not-valid-multi-character": "Invalid use of a multi-level wildcard character", - "single-level-wildcards-hint": "[+] is suitable for any topic filter level. Ex.: v1/devices/+/telemetry or +/devices/+/attributes.", - "multi-level-wildcards-hint": "[#] can replace the topic filter itself and must be the last symbol of the topic. Ex.: # or v1/devices/me/#.", + "mqtt-enable-compatibility-with-json-payload-format": "다른 페이로드 형식과의 호환성 사용.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "사용하면 플랫폼은 기본적으로 Protobuf 페이로드 형식을 사용합니다. 파싱이", + "mqtt-use-json-format-for-default-downlink-topics": "기본 다운링크 토픽에 JSON 형식 사용", + "mqtt-use-json-format-for-default-downlink-topics-hint": "사용하면 플랫폼은 속성 및 RPC를 푸시하는 데 JSON 페이로드 형식을 사용합니다.", + "mqtt-send-ack-on-validation-exception": "PUBLISH 메시지 유효성 검사 실패 시 PUBACK 전송", + "mqtt-send-ack-on-validation-exception-hint": "기본적으로 플랫폼은 메시지 유효성 검사 실패 시 MQTT 세션을 닫습니다.", + "mqtt-protocol-version": "프로토콜 버전", + "snmp-add-mapping": "SNMP 매핑 추가", + "snmp-mapping-not-configured": "OID를 시계열/텔레메트리에 매핑하도록 구성되지 않음", + "snmp-timseries-or-attribute-name": "매핑을 위한 시계열/속성 이름", + "snmp-timseries-or-attribute-type": "매핑을 위한 시계열/속성 유형", + "snmp-method-pdu-type-get-request": "GetRequest", + "snmp-method-pdu-type-get-next-request": "GetNextRequest", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "페이로드 유형은 필수 항목입니다.", + "coap-device-type": "CoAP 장치 유형", + "coap-device-payload-type": "CoAP 장치 페이로드", + "coap-device-type-required": "CoAP 장치 유형은 필수 항목입니다.", + "coap-device-type-default": "기본값", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "단일 레벨 [+] 및 다중 레벨 [#] 와일드카드가 지원됩니다.", + "telemetry-topic-filter": "텔레메트리 토픽 필터", + "telemetry-topic-filter-required": "텔레메트리 토픽 필터는 필수 항목입니다.", + "attributes-topic-filter": "속성 게시 토픽 필터", + "attributes-subscribe-topic-filter": "속성 구독 토픽 필터", + "attributes-topic-filter-required": "속성 게시 토픽 필터는 필수 항목입니다.", + "attributes-subscribe-topic-filter-required": "속성 구독 토픽은 필수 항목입니다.", + "telemetry-proto-schema": "텔레메트리 프로토 스키마", + "telemetry-proto-schema-required": "텔레메트리 프로토 스키마는 필수 항목입니다.", + "attributes-proto-schema": "속성 프로토 스키마", + "attributes-proto-schema-required": "속성 프로토 스키마는 필수 항목입니다.", + "rpc-response-proto-schema": "RPC 응답 프로토 스키마", + "rpc-response-proto-schema-required": "RPC 응답 프로토 스키마는 필수 항목입니다.", + "rpc-response-topic-filter": "RPC 응답 토픽 필터", + "rpc-response-topic-filter-required": "RPC 응답 토픽 필터는 필수 항목입니다.", + "rpc-request-proto-schema": "RPC 요청 프로토 스키마", + "rpc-request-proto-schema-required": "RPC 요청 프로토 스키마는 필수 항목입니다.", + "rpc-request-proto-schema-hint": "RPC 요청 메시지에는 항상 다음 필드가 있어야 합니다: string method = 1; int32 requestId", + "not-valid-pattern-topic-filter": "유효하지 않은 패턴 토픽 필터", + "not-valid-single-character": "단일 수준 와일드카드 문자의 잘못된 사용", + "not-valid-multi-character": "다중 수준 와일드카드 문자의 잘못된 사용", + "single-level-wildcards-hint": "[+]는 모든 토픽 필터 레벨에 적합합니다. 예: v1/devices/+/telemetry 또는 +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#]는 토픽 필터 자체를 대체할 수 있으며 토픽의 마지막 기호여야 합니다. 예: # 또는 v1/devices/me/#.", "alarm-rules": "알람 규칙", - "alarm-rules-with-count": "Alarm rules ({{count}})", - "no-alarm-rules": "No alarm rules configured", - "add-alarm-rule": "Add alarm rule", - "edit-alarm-rule": "Edit alarm rule", - "alarm-type": "Alarm type", - "alarm-type-required": "Alarm type is required.", - "alarm-type-unique": "Alarm type must be unique within the device profile alarm rules.", - "create-alarm-pattern": "Create {{alarmType}} alarm", - "create-alarm-rules": "Create alarm rules", - "no-create-alarm-rules": "No create conditions configured", - "add-create-alarm-rule-prompt": "Please add create alarm rule", - "clear-alarm-rule": "Clear alarm rule", - "no-clear-alarm-rule": "No clear condition configured", - "add-create-alarm-rule": "Add create condition", - "add-clear-alarm-rule": "Add clear condition", - "select-alarm-severity": "Select alarm severity", - "alarm-severity-required": "Alarm severity is required.", - "condition-duration": "Condition duration", - "condition-duration-value": "Duration value", - "condition-duration-time-unit": "Time unit", - "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", - "condition-duration-value-pattern": "Duration value should be integers.", - "condition-duration-value-required": "Duration value is required.", - "condition-duration-time-unit-required": "Time unit is required.", - "advanced-settings": "Advanced settings", - "propagate-alarm": "Propagate alarm", - "alarm-rule-relation-types-list": "Relation types", - "alarm-rule-relation-types-list-hint": "Defines relation types to filter the related entities. If not set, the alarm will be propagated to all related entities.", - "alarm-rule-condition": "Alarm rule condition", - "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", - "edit-alarm-rule-condition": "Edit alarm rule condition", + "alarm-rules-with-count": "알람 규칙 ({{count}})", + "no-alarm-rules": "구성된 알람 규칙 없음", + "add-alarm-rule": "알람 규칙 추가", + "edit-alarm-rule": "알람 규칙 편집", + "alarm-type": "알람 유형", + "alarm-type-required": "알람 유형은 필수 항목입니다.", + "alarm-type-unique": "알람 유형은 장치 프로필 알람 규칙 내에서 고유해야 합니다.", + "alarm-type-max-length": "알람 유형은 256자 미만이어야 합니다.", + "create-alarm-pattern": "{{alarmType}} 알람 생성", + "create-alarm-rules": "알람 규칙 생성", + "no-create-alarm-rules": "구성된 생성 조건 없음", + "add-create-alarm-rule-prompt": "알람 생성 규칙을 추가하세요.", + "clear-alarm-rule": "알람 규칙 지우기", + "no-clear-alarm-rule": "지움 조건이 구성되지 않음", + "add-create-alarm-rule": "생성 조건 추가", + "add-clear-alarm-rule": "지움 조건 추가", + "select-alarm-severity": "알람 심각도 선택", + "alarm-severity-required": "알람 심각도는 필수 항목입니다.", + "condition-duration": "조건 기간", + "condition-duration-value": "기간 값", + "condition-duration-time-unit": "시간 단위", + "condition-duration-value-range": "기간 값은 1에서 2147483647 범위여야 합니다.", + "condition-duration-value-pattern": "기간 값은 정수여야 합니다.", + "condition-duration-value-required": "기간 값은 필수 항목입니다.", + "condition-duration-time-unit-required": "시간 단위는 필수 항목입니다.", + "advanced-settings": "고급 설정", + "alarm-rule-additional-info": "추가 정보", + "edit-alarm-rule-additional-info": "추가 정보 수정", + "alarm-rule-additional-info-placeholder": "알람 세부 정보 내에 표시할 설명과 조정 사항을 여기에 입력하세요.", + "alarm-rule-additional-info-hint": "힌트: ${keyName
누락된 규칙 체인 목록:
{{missingRuleChains}}", + "upgrade-instructions": "업그레이드 지침", + "widget-datasource-error": "이 위젯은 엣지 엔티티 데이터소스만 지원합니다", + "connected": "연결됨", + "disconnected": "연결 해제됨" + }, + "edge-event": { + "type-dashboard": "대시보드", + "type-asset": "자산", + "type-device": "장치", + "type-device-profile": "장치 프로필", + "type-asset-profile": "자산 프로필", + "type-entity-view": "엔티티 뷰", + "type-alarm": "알람", + "type-rule-chain": "규칙 사슬", + "type-rule-chain-metadata": "규칙 사슬 메타데이터", + "type-edge": "엣지", + "type-user": "사용자", + "type-tenant": "테넌트", + "type-tenant-profile": "테넌트 프로필", + "type-customer": "고객", + "type-relation": "관계", + "type-widgets-bundle": "위젯 번들", + "type-widgets-type": "위젯 유형", + "type-admin-settings": "관리자 설정", + "type-ota-package": "OTA 패키지", + "type-queue": "큐", + "action-type-added": "추가됨", + "action-type-deleted": "삭제됨", + "action-type-updated": "수정됨", + "action-type-post-attributes": "속성 게시", + "action-type-attributes-updated": "속성 업데이트됨", + "action-type-attributes-deleted": "속성 삭제됨", + "action-type-timeseries-updated": "시계열 업데이트됨", + "action-type-credentials-updated": "자격 증명 업데이트됨", + "action-type-assigned-to-customer": "고객에게 할당", + "action-type-unassigned-from-customer": "지정된 고객 해제", + "action-type-relation-add-or-update": "관계 추가 또는 업데이트", + "action-type-relation-deleted": "관계 삭제됨", + "action-type-rpc-call": "RPC 호출", + "action-type-alarm-ack": "알람 확인", + "action-type-alarm-clear": "알람 해제", + "action-type-alarm-assigned": "알람 할당됨", + "action-type-alarm-unassigned": "알람 할당 해제", + "action-type-assigned-to-edge": "엣지에 할당됨", + "action-type-unassigned-from-edge": "엣지에서 할당 해제됨", + "action-type-credentials-request": "자격 증명 요청", + "action-type-entity-merge-request": "엔티티 병합 요청" }, "error": { "unable-to-connect": "서버에 연결할 수 없습니다! 인터넷 연결을 확인하십시오.", @@ -1073,58 +3021,72 @@ "unknown-error": "알 수 없는 오류" }, "entity": { - "entity": "개체", - "entities": "개체", - "aliases": "개체 별명", - "entity-alias": "개체 별명", - "unable-delete-entity-alias-title": "개체의 별명을 삭제할 수 없습니다.", - "unable-delete-entity-alias-text": "개체 별명 '{{entityAlias}}'은 다음 위젯에서 사용되고 있어 삭제할 수 없습니다:
{{widgetsList}}", - "duplicate-alias-error": "별명 '{{alias}}'은 이미 사용중 입니다.
대시보드 내에서 개체의 별명은 중복될 수 없습니다.", + "entity": "엔티티", + "entities": "엔티티", + "entities-count": "엔티티 수", + "alarms-count": "알람 수", + "aliases": "엔티티 별명", + "aliases-short": "별명", + "entity-alias": "엔티티 별명", + "unable-delete-entity-alias-title": "엔티티의 별명을 삭제할 수 없습니다.", + "unable-delete-entity-alias-text": "엔티티 별명 '{{entityAlias}}'은 다음 위젯에서 사용되고 있어 삭제할 수 없습니다:
{{widgetsList}}", + "duplicate-alias-error": "별명 '{{alias}}'은 이미 사용중 입니다.
대시보드 내에서 엔티티의 별명은 중복될 수 없습니다.", "missing-entity-filter-error": "별명 '{{alias}}'에 대한 필터가 지정되지 않았습니다.", "configure-alias": "별명 '{{alias}}' 설정", "alias": "별명", - "alias-required": "개체 별명을 입력하세요.", - "remove-alias": "개체 별명 삭제", - "add-alias": "개체 별명 추가", - "entity-list": "개체 목록", - "entity-type": "개체 유형", - "entity-types": "개체 유형", - "entity-type-list": "개체 유형 목록", - "any-entity": "모든 개체", - "enter-entity-type": "개체 유형 입력", - "no-entities-matching": "'{{entity}}'에 해당되는 개체를 찾을 수 없습니다.", - "no-entity-types-matching": "'{{entityType}}'에 해당되는 개체 유형을 찾을 수 없습니다.", + "alias-required": "엔티티 별명을 입력하세요.", + "remove-alias": "엔티티 별명 삭제", + "add-alias": "엔티티 별명 추가", + "edit-alias": "엔티티 별명 편집", + "entity-list": "엔티티 목록", + "entity-type": "엔티티 유형", + "entity-types": "엔티티 유형", + "entity-type-list": "엔티티 유형 목록", + "any-entity": "모든 엔티티", + "add-entity-type": "엔티티 유형 추가", + "enter-entity-type": "엔티티 유형 입력", + "no-entities-matching": "'{{entity}}'에 해당되는 엔티티를 찾을 수 없습니다.", + "no-entities-text": "아무 엔티티도 없습니다", + "no-entity-types-matching": "'{{entityType}}'에 해당되는 엔티티 유형을 찾을 수 없습니다.", "name-starts-with": "다음으로 시작하는 이름", + "help-text": "필요에 따라 '%'를 사용하세요: '%entity_name_contains%', '%entity_name_ends', 'entity_starts_with%'.", "use-entity-name-filter": "필터 사용", - "entity-list-empty": "아무 개체도 선택되지 않았습니다.", - "entity-name-filter-required": "개체 이름 필터를 입력하세요.", - "entity-name-filter-no-entity-matched": "'{{entity}}'로 시작되는 개체를 찾을 수 없습니다.", + "entity-list-empty": "아무 엔티티도 선택되지 않았습니다.", + "entity-type-list-required": "최소 하나의 엔티티 유형을 선택해야 합니다.", + "entity-name-filter-required": "엔티티 이름 필터를 입력하세요.", + "entity-name-filter-no-entity-matched": "'{{entity}}'로 시작되는 엔티티를 찾을 수 없습니다.", "all-subtypes": "모두", - "select-entities": "선택된 개체", - "no-aliases-found": "아무 개체도 없습니다.", - "no-alias-matching": "개체 '{{alias}}'을(를) 찾을 수 없습니다.", + "select-entities": "선택된 엔티티", + "no-aliases-found": "아무 엔티티도 없습니다.", + "no-alias-matching": "엔티티 '{{alias}}'을(를) 찾을 수 없습니다.", "create-new-alias": "새로 별명 만들기", + "create-new": "새로 만들기", "key": "키", "key-name": "키 이름", "no-keys-found": "아무 키도 찾을 수 없습니다.", "no-key-matching": "'{{key}}'를 찾을 수 없습니다.", "create-new-key": "새로 키 만들기", "type": "유형", - "type-required": "개체의 유형을 입력하세요.", + "type-required": "엔티티의 유형을 입력하세요.", "type-device": "장치", "type-devices": "장치", "list-of-devices": "{ count, plural, =1 {1 개 장치} other {# 개 장치} }", "device-name-starts-with": "이름이 '{{prefix}}'로 시작되는 장치", - "type-device-profile": "Device profile", - "type-device-profiles": "Device profiles", - "list-of-device-profiles": "{ count, plural, =1 {1 개 장치 프로파일} other {# 개 장치 프로파일 목록} }", - "device-profile-name-starts-with": "Device profiles whose names start with '{{prefix}}'", + "type-device-profile": "장치 프로필", + "type-device-profiles": "장치 프로필", + "clear-selected-profiles": "선택한 프로필 지우기", + "list-of-device-profiles": "{ count, plural, =1 {1 개 장치 프로필} other {# 개 장치 프로필 목록} }", + "device-profile-name-starts-with": "이름이 '{{prefix}}'로 시작하는 장치 프로필", + "type-asset-profile": "자산 프로필", + "type-asset-profiles": "자산 프로필", + "list-of-asset-profiles": "{ count, plural, =1 {하나의 자산 프로필} other {#개 자산 프로필 목록} }", + "asset-profile-name-starts-with": "이름이 '{{prefix}}'로 시작하는 자산 프로필", "type-asset": "자산", "type-assets": "자산", "list-of-assets": "{ count, plural, =1 {1 개 자산} other {# 개 자산} }", "asset-name-starts-with": "이름이 '{{prefix}}'로 시작되는 자산", - "type-entity-view": "개체 보기", - "type-entity-views": "개체 보기", + "type-entity-view": "엔티티 뷰", + "type-entity-views": "엔티티 뷰", "list-of-entity-views": "{ count, plural, =1 {1 개 자산 보기} other {# 개 자산 보기} }", "entity-view-name-starts-with": "이름이 '{{prefix}}'로 시작되는 자산 보기", "type-rule": "규칙", @@ -1139,14 +3101,14 @@ "type-tenants": "테넌트", "list-of-tenants": "{ count, plural, =1 {1 개 테넌트} other {# 개 테넌트} }", "tenant-name-starts-with": "이름이 '{{prefix}}'로 시작되는 테넌트", - "type-tenant-profile": "테넌트 프로파일", - "type-tenant-profiles": "테넌트 프로파일", - "list-of-tenant-profiles": "{ count, plural, =1 {1 개 테넌트 프로파일} other {# 개 테넌트 프로파일 목록} }", - "tenant-profile-name-starts-with": "'이름이 {{prefix}}'로 시작하는 테넌트 프로파일", - "type-customer": "커스터머", - "type-customers": "커스터머", - "list-of-customers": "{ count, plural, =1 {1 개 커스터머} other {# 개 커스터머} }", - "customer-name-starts-with": "이름이 '{{prefix}}'로 시작되는 커스터머", + "type-tenant-profile": "테넌트 프로필", + "type-tenant-profiles": "테넌트 프로필", + "list-of-tenant-profiles": "{ count, plural, =1 {1 개 테넌트 프로필} other {# 개 테넌트 프로필 목록} }", + "tenant-profile-name-starts-with": "'이름이 {{prefix}}'로 시작하는 테넌트 프로필", + "type-customer": "고객", + "type-customers": "고객", + "list-of-customers": "{ count, plural, =1 {1 개 고객} other {# 개 고객} }", + "customer-name-starts-with": "이름이 '{{prefix}}'로 시작되는 고객", "type-user": "사용자", "type-users": "사용자", "list-of-users": "{ count, plural, =1 {1 개 사용자} other {# 개 사용자} }", @@ -1167,19 +3129,84 @@ "type-rulenodes": "규칙 노드", "list-of-rulenodes": "{ count, plural, =1 {1 개 규칙 노드} other {# 개 규칙 노드의 목록} }", "rulenode-name-starts-with": "이름이 '{{prefix}}'로 시작되는 규칙 노트", - "type-current-customer": "현재 커스터머", + "type-api-key": "API 키", + "type-api-keys": "API 키", + "type-current-customer": "현재 고객", "type-current-tenant": "현재 테넌트", "type-current-user": "현재 사용자", "type-current-user-owner": "현재 소유자", - "search": "개체 검색", - "selected-entities": "{ count, plural, =1 {1 개체} other {# 개체} } 선택됨", - "entity-name": "개체 이름", - "entity-label": "개체 라벨", - "details": "개체 상세", - "no-entities-prompt": "아무 개체도 없습니다", + "type-calculated-field": "계산 필드", + "type-calculated-fields": "계산 필드", + "type-ai-model": "AI 모델", + "type-ai-models": "AI 모델", + "type-widgets-bundle": "위젯 번들", + "type-widgets-bundles": "위젯 번들", + "list-of-widgets-bundles": "{ count, plural, =1 {하나의 위젯 번들} other {#개 위젯 번들 목록} }", + "type-widget": "위젯", + "type-widgets": "위젯", + "list-of-widgets": "{ count, plural, =1 {1개 위젯} other {#개 위젯 목록} }", + "search": "엔티티 검색", + "selected-entities": "{ count, plural, =1 {1 엔티티} other {# 엔티티} } 선택됨", + "entity-name": "엔티티 이름", + "entity-label": "엔티티 라벨", + "details": "엔티티 상세", + "no-entities-prompt": "아무 엔티티도 없습니다", "no-data": "표시할 데이터가 없습니다", + "show-all-columns": "모두 표시", "columns-to-display": "표시할 컬럼", - "type-api-usage-state": "Api 사용량 상태" + "type-api-usage-state": "Api 사용량 상태", + "type-edge": "엣지", + "type-edges": "엣지", + "list-of-edges": "{ count, plural, =1 {1개 Edge} other {#개 Edge 목록} }", + "edge-name-starts-with": "이름이 '{{prefix}}'로 시작하는 Edge", + "version-conflict": { + "message": "기존 버전을 덮어쓰거나 변경 사항을 삭제하고 최신 버전을 로드하시겠습니까?", + "link": "이것을 사용하여 {{entityType}}의 버전을 다운로드할 수 있습니다.", + "overwrite": "버전 덮어쓰기", + "discard": "변경 취소" + }, + "type-tb-resource": "리소스", + "type-tb-resources": "리소스", + "list-of-tb-resources": "{ count, plural, =1 {1개 리소스} other {#개 리소스 목록} }", + "type-ota-package": "OTA 패키지", + "type-ota-packages": "OTA 패키지", + "list-of-ota-packages": "{ count, plural, =1 {1개 OTA 패키지} other {#개 OTA 패키지 목록} }", + "type-rpc": "RPC", + "type-queue": "큐", + "type-queue-stats": "큐 통계", + "type-queues-stats": "큐 통계", + "type-notification": "알림", + "type-notification-rule": "알림 규칙", + "type-notification-rules": "알림 규칙", + "list-of-notification-rules": "{ count, plural, =1 {하나의 알림 규칙} other {#개 알림 규칙 목록} }", + "type-notification-target": "알림 수신자", + "type-notification-targets": "알림 수신자", + "list-of-notification-targets": "{ count, plural, =1 {하나의 알림 수신자} other {#개 알림 수신자 목록} }", + "type-notification-request": "알림 요청", + "type-notification-template": "알림 템플릿", + "type-notification-templates": "알림 템플릿", + "list-of-notification-templates": "{ count, plural, =1 {하나의 알림 템플릿} other {#개 알림 템플릿 목록} }", + "link": "링크", + "type-oauth2-client": "OAuth 2.0 클라이언트", + "type-oauth2-clients": "OAuth 2.0 클라이언트", + "list-of-oauth2-clients": "{ count, plural, =1 {하나의 OAuth 2.0 클라이언트} other {#개 OAuth 2.0 클라이언트 목록} }", + "type-domain": "도메인", + "type-domains": "도메인", + "list-of-domains": "{ count, plural, =1 {1개 도메인} other {#개 도메인 목록} }", + "type-mobile-app": "모바일 애플리케이션", + "type-mobile-apps": "모바일 애플리케이션", + "list-of-mobile-apps": "{ count, plural, =1 {하나의 모바일 애플리케이션} other {#개 모바일 애플리케이션 목록} }", + "type-mobile-app-bundle": "모바일 번들", + "type-mobile-app-bundles": "모바일 번들", + "list-of-mobile-app-bundles": "{ count, plural, =1 {하나의 모바일 번들} other {#개 모바일 번들 목록} }", + "limit-reached": "한도 도달", + "limit-reached-text": "{{ entities }}의 한도에 도달했습니다. 더 추가하려면 시스템 관리자에게 {{ entity }} 한도 증가를 요청하세요.", + "request-limit-increase": "한도 증가 요청", + "request-sysadmin-text": "시스템 관리자입니까?", + "login-here": "여기에 로그인", + "to-increase-limit": "하여 한도를 늘리세요.", + "increase-limit-request-sent-title": "시스템 관리자에게 한도 증가를 요청하는 자동 요청을 보냈습니다", + "increase-limit-request-sent-text": "요청을 검토하고 설정을 업데이트할 시간을 기다려 주세요. 변경 사항을 보려면 이 페이지를 새로고침해야 할 수 있습니다." }, "entity-field": { "created-time": "생성 일시", @@ -1187,7 +3214,7 @@ "type": "유형", "first-name": "이름", "last-name": "성", - "email": "Email", + "email": "이메일", "title": "제목", "country": "국가", "state": "주", @@ -1196,88 +3223,94 @@ "address2": "주소 2", "zip": "우편번호", "phone": "전화", - "label": "라벨" + "label": "라벨", + "queue-name": "큐 이름", + "service-id": "서비스 ID", + "owner-name": "소유자 이름", + "owner-type": "소유자 유형" }, "entity-view": { - "entity-view": "개체 보기", - "entity-view-required": "개체 보기를 입력하세요.", - "entity-views": "개체 보기", - "management": "개체 보기 관리", - "view-entity-views": "개체 보기 보기", - "entity-view-alias": "개체 보기 별명", - "aliases": "개체 보기 별명", - "no-alias-matching": "'{{alias}}' not found.", - "no-aliases-found": "No aliases found.", - "no-key-matching": "'{{key}}' not found.", - "no-keys-found": "No keys found.", - "create-new-alias": "Create a new one!", - "create-new-key": "Create a new one!", - "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
Entity View aliases must be unique within the dashboard.", - "configure-alias": "Configure '{{alias}}' alias", - "no-entity-views-matching": "No entity views matching '{{entity}}' were found.", - "public": "Public", - "alias": "Alias", - "alias-required": "Entity View alias is required.", - "remove-alias": "Remove entity view alias", - "add-alias": "Add entity view alias", - "name-starts-with": "Entity View name starts with", - "entity-view-list": "Entity View list", - "use-entity-view-name-filter": "Use filter", - "entity-view-list-empty": "No entity views selected.", - "entity-view-name-filter-required": "Entity view name filter is required.", - "entity-view-name-filter-no-entity-view-matched": "No entity views starting with '{{entityView}}' were found.", - "add": "Add Entity View", - "entity-view-public": "Entity view is public", - "assign-to-customer": "Assign to customer", - "assign-entity-view-to-customer": "Assign Entity View(s) To Customer", - "assign-entity-view-to-customer-text": "Please select the entity views to assign to the customer", - "no-entity-views-text": "No entity views found", - "assign-to-customer-text": "Please select the customer to assign the entity view(s)", - "entity-view-details": "개체 보기 상세 정보", - "add-entity-view-text": "새로운 개체 보기 추가", - "delete": "Delete entity view", - "assign-entity-views": "개체 보기 할당", - "assign-entity-views-text": "{ count, plural, =1 {1 개 개체 보기} other {# 개 개체 보기} }를 커스터머에 할당", - "delete-entity-views": "Delete entity views", - "unassign-from-customer": "Unassign from customer", - "unassign-entity-views": "Unassign entity views", - "unassign-entity-views-action-title": "{ count, plural, =1 {1 개 개체 보기} other {# 개 개체 보기} }를 커스터머로 부터 할당 해제", - "assign-new-entity-view": "Assign new entity view", - "delete-entity-view-title": "Are you sure you want to delete the entity view '{{entityViewName}}'?", - "delete-entity-view-text": "Be careful, after the confirmation the entity view and all related data will become unrecoverable.", - "delete-entity-views-title": "Are you sure you want to delete { count, plural, =1 {1 개 개체 보기} other {# 개 개체 보기} }?", - "delete-entity-views-action-title": "Delete { count, plural, =1 {1 개 개체 보기} other {# 개 개체 보기} }", - "delete-entity-views-text": "Be careful, after the confirmation all selected entity views will be removed and all related data will become unrecoverable.", - "unassign-entity-view-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", - "unassign-entity-view-text": "After the confirmation the entity view will be unassigned and won't be accessible by the customer.", - "unassign-entity-view": "Unassign entity view", - "unassign-entity-views-title": "Are you sure you want to unassign { count, plural, =1 {1 개 개체 보기} other {# 개 개체 보기} }?", - "unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.", - "entity-view-type": "개체 보기 유형", - "entity-view-type-required": "Entity View type is required.", - "select-entity-view-type": "Select entity view type", - "enter-entity-view-type": "Enter entity view type", - "any-entity-view": "Any entity view", - "no-entity-view-types-matching": "No entity view types matching '{{entitySubtype}}' were found.", - "entity-view-type-list-empty": "No entity view types selected.", - "entity-view-types": "Entity View types", - "created-time": "Created time", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", + "entity-view": "엔티티 뷰", + "entity-view-required": "엔티티 뷰를 입력하세요.", + "entity-views": "엔티티 뷰", + "management": "엔티티 뷰 관리", + "view-entity-views": "엔티티 뷰 보기", + "entity-view-alias": "엔티티 뷰 별명", + "aliases": "엔티티 뷰 별명", + "no-alias-matching": "'{{alias}}' 를 찾을 수 없습니다.", + "no-aliases-found": "별명이 없습니다.", + "no-key-matching": "'{{key}}' 를 찾을 수 없습니다.", + "no-keys-found": "Key가 없습니다.", + "create-new-alias": "새로 만들기!", + "create-new-key": "새로 만들기!", + "duplicate-alias-error": "중복 별명 '{{alias}}'이(가) 발견되었습니다.
엔티티 뷰 별명은 대시보드 내에서 고유해야 합니다.", + "configure-alias": "'{{alias}}' 별명 구성", + "no-entity-views-matching": "'{{entity}}'와(과) 일치하는 엔티티 뷰를 찾을 수 없습니다.", + "public": "공개", + "alias": "별명", + "alias-required": "엔티티 뷰 별명은 필수 항목입니다.", + "remove-alias": "엔티티 뷰 별명 제거", + "add-alias": "엔티티 뷰 별명 추가", + "name-starts-with": "엔티티 뷰 이름 표현식", + "help-text": "필요에 따라 '%'를 사용하세요: '%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_starts_with%'.", + "entity-view-list": "엔티티 뷰 목록", + "use-entity-view-name-filter": "필터 사용", + "entity-view-list-empty": "선택된 엔티티 뷰 없음.", + "entity-view-name-filter-required": "엔티티 뷰 이름 필터는 필수 항목입니다.", + "entity-view-name-filter-no-entity-view-matched": "'{{entityView}}'로 시작하는 엔티티 뷰를 찾을 수 없습니다.", + "add": "엔티티 뷰 추가", + "entity-view-public": "엔티티 뷰가 공개됨", + "assign-to-customer": "고객에게 자산 할당", + "assign-entity-view-to-customer": "고객에 엔티티 뷰 할당", + "assign-entity-view-to-customer-text": "고객에 할당할 엔티티 뷰를 선택하세요", + "assign-entity-view-to-edge-title": "엣지에 엔티티 뷰 할당", + "no-entity-views-text": "엔티티 뷰를 찾을 수 없음", + "assign-to-customer-text": "엔티티 뷰를 할당할 고객을 선택하세요", + "entity-view-details": "엔티티 뷰 상세 정보", + "add-entity-view-text": "새로운 엔티티 뷰 추가", + "delete": "엔티티 뷰 삭제", + "assign-entity-views": "엔티티 뷰 할당", + "assign-entity-views-text": "{ count, plural, =1 {1 개 엔티티 뷰} other {# 개 엔티티 뷰} }를 고객에 할당", + "delete-entity-views": "엔티티 뷰 삭제", + "unassign-from-customer": "고객 할당 해제", + "unassign-entity-views": "엔티티 뷰 할당 해제", + "unassign-entity-views-action-title": "{ count, plural, =1 {1 개 엔티티 뷰} other {# 개 엔티티 뷰} }를 고객으로 부터 할당 해제", + "assign-new-entity-view": "새 엔티티 뷰 할당", + "delete-entity-view-title": "엔티티 뷰 '{{entityViewName}}'을(를) 삭제하시겠습니까?", + "delete-entity-view-text": "주의: 확인 후 엔티티 뷰와 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-entity-views-title": "정말 { count, plural, =1 {1개의 엔티티 뷰} other {#개의 엔티티 뷰} }를 삭제하시겠습니까?", + "delete-entity-views-action-title": "Delete { count, plural, =1 {1 개 엔티티 뷰} other {# 개 엔티티 뷰} }", + "delete-entity-views-text": "주의: 확인 후 선택한 모든 엔티티 뷰가 제거되고 모든 관련 데이터를 복구할 수 없게 됩니다.", + "unassign-entity-view-title": "엔티티 뷰 '{{entityViewName}}'을(를) 엣지에서 할당 해제하시겠습니까?", + "unassign-entity-view-text": "확인 후 엔티티 뷰가 할당 해제되어 고객이 액세스할 수 없습니다.", + "unassign-entity-view": "엔티티 뷰 할당 해제", + "unassign-entity-views-title": "정말 { count, plural, =1 {1개의 엔티티 뷰} other {#개의 엔티티 뷰} }를 할당 해제하시겠습니까?", + "unassign-entity-views-text": "확인 후 선택한 모든 엔티티 뷰가 할당 해제되어 고객이 액세스할 수 없습니다.", + "entity-view-type": "엔티티 뷰 유형", + "entity-view-type-required": "엔티티 뷰 유형은 필수 항목입니다.", + "select-entity-view-type": "엔티티 뷰 유형 선택", + "enter-entity-view-type": "엔티티 뷰 유형 입력", + "any-entity-view": "모든 엔티티 뷰", + "no-entity-view-types-matching": "'{{entitySubtype}}'와(과) 일치하는 엔티티 뷰 유형을 찾을 수 없습니다.", + "entity-view-type-list-empty": "선택된 엔티티 뷰 유형 없음.", + "entity-view-types": "엔티티 뷰 유형", + "created-time": "생성 일시", + "name": "이름", + "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "type-max-length": "엔티티 뷰 유형은 256자 미만이어야 합니다.", + "description": "설명", "events": "이벤트", "details": "상세 정보", - "copyId": "개체 보기 ID 복사", - "idCopiedMessage": "개체 보기 ID가 클립보드에 복사되었습니다", - "assignedToCustomer": "할당된 커스터머", - "unable-entity-view-device-alias-title": "개체 보기 별명을 삭제할 수 없습니다", - "unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", - "select-entity-view": "개체 보기 선택", - "make-public": "개체 보기를 공개로 전환", - "make-private": "개체 보기를 비공개로 전환", - "start-date": "시작 날짜", + "copyId": "엔티티 뷰 ID 복사", + "idCopiedMessage": "엔티티 뷰 ID가 클립보드에 복사되었습니다", + "assignedToCustomer": "할당된 고객", + "unable-entity-view-device-alias-title": "엔티티 뷰 별명을 삭제할 수 없습니다", + "unable-entity-view-device-alias-text": "장치 별명 '{{entityViewAlias}}'은(는) 다음 위젯에서 사용 중이므로 삭제할 수 없습니다:
{{widgetsList}}", + "select-entity-view": "엔티티 뷰 선택", + "make-public": "엔티티 뷰를 공개로 전환", + "make-private": "엔티티 뷰를 비공개로 전환", "start-ts": "시작 시간", - "end-date": "종료 날짜", "end-ts": "종료 시간", "date-limits": "날짜 제한", "client-attributes": "클라이언트 속성", @@ -1288,34 +3321,49 @@ "shared-attributes-placeholder": "공유 속성", "server-attributes-placeholder": "서버 속성", "timeseries-placeholder": "시계열", - "target-entity": "대상 개체", + "target-entity": "대상 엔티티", "attributes-propagation": "속성 전파", - "attributes-propagation-hint": "Entity View will automatically copy specified attributes from Target Entity each time you save or update this entity view. For performance reasons target entity attributes are not propagated to entity view on each attribute change. You can enable automatic propagation by configuring \"copy to view\" rule node in your rule chain and linking \"Post attributes\" and \"Attributes Updated\" messages to the new rule node.", + "attributes-propagation-hint": "엔티티 뷰는 이 엔티티 뷰를 저장하거나 업데이트할 때마다 대상 엔티티에서 지정된 속성을 자동으로 복사합니다. 성능상의 이유로 대상 엔티티 속성은 각 속성 변경 시 엔티티 뷰에 전파되지 않습니다. 규칙 체인에서 \"뷰로 복사\" 규칙 노드를 구성하고 \"속성 게시\" 및 \"속성 업데이트됨\" 메시지를 새 규칙 노드에 연결하여 자동 전파를 활성화할 수 있습니다.", "timeseries-data": "시계열 데이터", - "timeseries-data-hint": "Configure timeseries data keys of the target entity that will be accessible to the entity view. This timeseries data is read-only.", - "make-public-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' public?", - "make-public-entity-view-text": "After the confirmation the entity view and all its data will be made public and accessible by others.", - "make-private-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' private?", - "make-private-entity-view-text": "After the confirmation the entity view and all its data will be made private and won't be accessible by others.", - "search": "Search entity views", - "selected-entity-views": "{ count, plural, =1 {1 개 개체 보기} other {# 개 개체 보기} } 선택됨" + "timeseries-data-hint": "엔티티 뷰에서 액세스할 수 있는 대상 엔티티의 시계열 데이터 키를 구성합니다. 이 시계열 데이터는 읽기 전용입니다.", + "search": "엔티티 뷰 검색", + "selected-entity-views": "{ count, plural, =1 {1 개 엔티티 뷰} other {# 개 엔티티 뷰} } 선택됨", + "make-public-entity-view-title": "엔티티 뷰 '{{entityViewName}}'을(를) 공개하시겠습니까?", + "make-public-entity-view-text": "확인 후 엔티티 뷰와 모든 데이터가 공개되어 다른 사용자가 액세스할 수 있습니다.", + "make-private-entity-view-title": "엔티티 뷰 '{{entityViewName}}'을(를) 비공개로 설정하시겠습니까?", + "make-private-entity-view-text": "확인 후 엔티티 뷰와 모든 데이터가 비공개되어 다른 사용자가 액세스할 수 없습니다.", + "assign-entity-view-to-edge": "엣지에 엔티티 뷰 할당", + "assign-entity-view-to-edge-text": "엣지에 할당할 엔티티 뷰를 선택하세요", + "unassign-entity-view-from-edge-title": "엔티티 뷰 '{{entityViewName}}'을(를) 엣지에서 할당 해제하시겠습니까?", + "unassign-entity-view-from-edge-text": "확인 후 엔티티 뷰가 할당 해제되어 엣지에서 액세스할 수 없습니다.", + "unassign-entity-views-from-edge-action-title": "엣지에서 { count, plural, =1 {1개 엔티티 뷰} other {#개 엔티티 뷰} } 할당 해제", + "unassign-entity-view-from-edge": "엔티티 뷰 할당 해제", + "unassign-entity-views-from-edge-title": "정말 { count, plural, =1 {1개의 엔티티 뷰} other {#개의 엔티티 뷰} }를 할당 해제하시겠습니까?", + "unassign-entity-views-from-edge-text": "확인 후 선택한 모든 엔티티 뷰가 할당 해제되어 엣지에서 액세스할 수 없습니다." }, "event": { "event-type": "이벤트 타입", + "events-filter": "이벤트 필터", + "clean-events": "이벤트 지우기", "type-error": "에러", "type-lc-event": "주기적 이벤트", "type-stats": "통계", "type-debug-rule-node": "디버그", "type-debug-rule-chain": "디버그", + "type-debug-calculated-field": "디버그", + "arguments": "인수", + "result": "결과", "no-events-prompt": "이벤트 없음", "error": "에러", "alarm": "알람", "event-time": "이벤트 발생 시간", "server": "서버", - "body": "Body", + "body": "본문", "method": "방법", "type": "유형", + "message": "메시지", "message-id": "메시지 ID", + "copy-message-id": "메시지 ID 복사", "message-type": "메시지 유형", "data-type": "데이터 유형", "relation-type": "관계 유형", @@ -1326,9 +3374,23 @@ "success": "성공", "failed": "실패", "messages-processed": "처리된 메시지", + "max-messages-processed": "처리된 최대 메시지 수", + "min-messages-processed": "처리된 최소 메시지 수", "errors-occurred": "오류가 발생했습니다", + "max-errors-occurred": "발생한 최대 오류 수", + "min-errors-occurred": "발생한 최소 오류 수", + "min-value": "최소 값은 0입니다.", "all-events": "모두", - "entity-type": "개체 유형" + "has-error": "오류 있음", + "entity-id": "엔티티 ID", + "copy-entity-id": "엔티티 ID 복사", + "entity-type": "엔티티 유형", + "clear-filter": "필터 지우기", + "clear-request-title": "모든 이벤트 지우기", + "clear-request-text": "모든 이벤트를 지우시겠습니까?", + "started": "시작됨", + "updated": "수정됨", + "stopped": "정지됨" }, "extension": { "extensions": "확장", @@ -1358,13 +3420,13 @@ "device-name-expression": "장치 이름 표현", "device-type-expression": "장치 유형 표현", "custom": "사용자 정의", - "to-double": "To Double", - "transformer": "Transformer", + "to-double": "실수로 변환", + "transformer": "변환기", "json-required": "Transformer json을(를) 입력하세요.", - "json-parse": "Unable to parse transformer json.", + "json-parse": "변환기 JSON을 파싱할 수 없습니다.", "attributes": "속성", "add-attribute": "속성 추가", - "add-map": "Add mapping element", + "add-map": "매핑 요소 추가", "timeseries": "시계열", "add-timeseries": "시계열 추가", "field-required": "필드를 입력하세요.", @@ -1373,7 +3435,7 @@ "host": "호스트", "port": "포트", "port-range": "포트를 1~65535 범위 내에서 입력하세요.", - "ssl": "Ssl", + "ssl": "SSL", "credentials": "인증", "username": "사용자 이름", "password": "비밀 번호", @@ -1386,23 +3448,23 @@ "cert": "인증 파일 *", "no-file": "아무 파일도 선택되지 않았습니다.", "drop-file": "여기로 파일을 드래그 하거나, 여기를 클릭해서 파일을 선택하세요.", - "mapping": "Mapping", - "topic-filter": "주제 필터", + "mapping": "매핑", + "topic-filter": "토픽 필터", "converter-type": "변환기 유형", - "converter-json": "Json", + "converter-json": "JSON", "json-name-expression": "장치 이름 JSON 표현", - "topic-name-expression": "장치 이름 주제 표현", + "topic-name-expression": "장치 이름 토픽 표현", "json-type-expression": "장치 유형 JSON 표현", - "topic-type-expression": "장치 유형 주제 표현", + "topic-type-expression": "장치 유형 토픽 표현", "attribute-key-expression": "속성 키 표현", "attr-json-key-expression": "속성 키 JSON 표현", - "attr-topic-key-expression": "속성 키 주제 표현", + "attr-topic-key-expression": "속성 키 토픽 표현", "request-id-expression": "요청 ID 표현", "request-id-json-expression": "요청 ID JSON 표현", - "request-id-topic-expression": "요청 ID 주제 표현", - "response-topic-expression": "응답 주제 표현", + "request-id-topic-expression": "요청 ID 토픽 표현", + "response-topic-expression": "응답 토픽 표현", "value-expression": "값 표현", - "topic": "주제", + "topic": "토픽", "timeout": "시간 제한(ms)", "converter-json-required": "JSON 변환기가 필요합니다.", "converter-json-parse": "JSON 변환기를 파싱할 수 없습니다.", @@ -1415,15 +3477,15 @@ "add-attribute-request": "속성 요청 추가", "attribute-updates": "속성 업데이트", "add-attribute-update": "속성 업데이트 추가", - "server-side-rpc": "Server side RPC", - "add-server-side-rpc-request": "Add server-side RPC request", - "device-name-filter": "Device name filter", - "attribute-filter": "Attribute filter", - "method-filter": "Method filter", - "request-topic-expression": "Request topic expression", - "response-timeout": "Response timeout in milliseconds", - "topic-expression": "Topic expression", - "client-scope": "Client scope", + "server-side-rpc": "서버 측 RPC", + "add-server-side-rpc-request": "서버 측 RPC 요청 추가", + "device-name-filter": "장치 이름 필터", + "attribute-filter": "속성 필터", + "method-filter": "메서드 필터", + "request-topic-expression": "요청 토픽 식", + "response-timeout": "응답 시간 초과 (밀리초)", + "topic-expression": "토픽 식", + "client-scope": "클라이언트 범위", "add-device": "장치 추가", "opc-server": "서버", "opc-add-server": "서버 추가", @@ -1432,7 +3494,7 @@ "opc-application-uri": "애플리케이션 URI", "opc-scan-period-in-seconds": "스캔 주기(초)", "opc-security": "보안", - "opc-identity": "Identity", + "opc-identity": "ID", "opc-keystore": "키스톤", "opc-type": "유형", "opc-keystore-type": "유형", @@ -1447,31 +3509,31 @@ "modbus-add-server-prompt": "서버/보조를 추가해주세요", "modbus-transport": "전송", "modbus-tcp-reconnect": "자동으로 재접속", - "modbus-rtu-over-tcp": "RTU over TCP", + "modbus-rtu-over-tcp": "TCP over RTU", "modbus-port-name": "시리얼 포트 이름", "modbus-encoding": "인코딩", - "modbus-parity": "Parity", + "modbus-parity": "패리티", "modbus-baudrate": "전송 속도", "modbus-databits": "데이터 비트", "modbus-stopbits": "정지 비트", "modbus-databits-range": "데이터 비트를 7~8 범위에서 입력하세요.", "modbus-stopbits-range": "정지 비트를 1~2 범위에서 입력하세요.", - "modbus-unit-id": "Unit ID", - "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", + "modbus-unit-id": "유닛 ID", + "modbus-unit-id-range": "유닛 ID는 1에서 247 범위여야 합니다.", "modbus-device-name": "장치 이름", "modbus-poll-period": "풀 주기 (ms)", "modbus-attributes-poll-period": "속성 풀 주기 (ms)", "modbus-timeseries-poll-period": "시계열 풀 주기 (ms)", "modbus-poll-period-range": "풀 주기를 양수로 입력하세요.", "modbus-tag": "태그", - "modbus-function": "Function", + "modbus-function": "함수", "modbus-register-address": "등록 주소", "modbus-register-address-range": "등록 주소를 0~65535 범위에서 입력하세요.", "modbus-register-bit-index": "비트 인덱스", "modbus-register-bit-index-range": "비트 인덱스를 0~15 범위에서 입력하세요.", "modbus-register-count": "등록 카운트", "modbus-register-count-range": "등록 카운트를 양수로 입력하세요.", - "modbus-byte-order": "Byte order", + "modbus-byte-order": "바이트 순서", "sync": { "status": "상태", "sync": "동기화", @@ -1487,91 +3549,110 @@ "file": "확장 파일", "invalid-file-error": "올바르지 않은 확장 파일" }, + "feature": { + "advanced-features": "고급 기능" + }, "filter": { - "add": "Add filter", - "edit": "Edit filter", - "name": "Filter name", - "name-required": "Filter name is required.", - "duplicate-filter": "Filter with same name is already exists.", - "filters": "Filters", - "unable-delete-filter-title": "Unable to delete filter", - "unable-delete-filter-text": "Filter '{{filter}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", - "duplicate-filter-error": "Duplicate filter found '{{filter}}'.
Filters must be unique within the dashboard.", - "missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.", - "filter": "필터", - "editable": "편집 가능", - "no-filters-found": "아무 필터도 찾을 수 없습니다.", - "no-filter-text": "필터가 특정되지 않았습니다", - "add-filter-prompt": "필터를 추가해주세요", - "no-filter-matching": "필터 '{{filter}}'를 찾을 수 없습니다.", - "create-new-filter": "새로 필터를 만드세요!", - "filter-required": "필터를 입력하세요.", - "operation": { - "operation": "Operation", - "equal": "같음", - "not-equal": "같이 않음", - "starts-with": "다음으로 시작함", - "ends-with": "다음으로 끝남", - "contains": "포함함", - "not-contains": "포함하지 않음", - "greater": "큼", - "less": "작음", - "greater-or-equal": "크거나 같음", - "less-or-equal": "작거나 같음", - "and": "그리고", - "or": "또는" - }, - "ignore-case": "ignore case", - "value": "값", - "remove-filter": "Remove filter", - "preview": "Filter preview", - "no-filters": "No filters configured", - "add-filter": "Add filter", - "add-complex-filter": "Add complex filter", - "add-complex": "Add complex", - "complex-filter": "Complex filter", - "edit-complex-filter": "Edit complex filter", - "edit-filter-user-params": "Edit filter predicate user parameters", - "filter-user-params": "Filter predicate user parameters", - "user-parameters": "User parameters", - "display-label": "Label to display", - "order-priority": "Field order priority", - "key-filter": "Key filter", - "key-filters": "Key filters", - "key-name": "Key name", - "key-name-required": "Key name is required.", - "key-type": { - "key-type": "Key type", - "attribute": "Attribute", - "timeseries": "Timeseries", - "entity-field": "Entity field" - }, - "value-type": { - "value-type": "Value type", - "string": "String", - "numeric": "Numeric", - "boolean": "Boolean", - "date-time": "Datetime" - }, - "value-type-required": "Key value type is required.", - "key-value-type-change-title": "Are you sure you want to change key value type?", - "key-value-type-change-message": "If you confirm new value type all entered key filters will be removed.", - "no-key-filters": "No key filters configured", - "add-key-filter": "Add key filter", - "remove-key-filter": "Remove key filter", - "edit-key-filter": "Edit key filter", - "date": "Date", - "time": "Time", - "current-tenant": "Current tenant", - "current-customer": "Current customer", - "current-user": "Current user", - "current-device": "현재 장치", - "default-value": "기본값", - "dynamic-source-type": "Dynamic source type", - "no-dynamic-value": "No dynamic value", - "source-attribute": "소스 속성", - "switch-to-dynamic-value": "Switch to dynamic value", - "switch-to-default-value": "Switch to default value" + "add": "필터 추가", + "edit": "필터 편집", + "name": "필터 이름", + "name-required": "필터 이름은 필수 항목입니다.", + "duplicate-filter": "같은 이름의 필터가 이미 존재합니다.", + "filters": "필터", + "unable-delete-filter-title": "필터를 삭제할 수 없습니다", + "unable-delete-filter-text": "필터 '{{filter}}'은(는) 다음 위젯에서 사용 중이므로 삭제할 수 없습니다:
{{widgetsList}}", + "duplicate-filter-error": "중복 필터 '{{filter}}'이(가) 발견되었습니다.
필터는 대시보드 내에서 고유해야 합니다.", + "missing-key-filters-error": "'{{filter}}' 필터에 키 필터가 없습니다.", + "filter": "필터", + "editable": "편집 가능", + "editable-hint": "사용자가 대시보드에서 필터 값을 변경할 수 있도록 허용.", + "no-filters-found": "아무 필터도 찾을 수 없습니다.", + "no-filter-text": "필터가 특정되지 않았습니다", + "add-filter-prompt": "필터를 추가해주세요", + "no-filter-matching": "필터 '{{filter}}'를 찾을 수 없습니다.", + "create-new-filter": "새로 필터를 만드세요!", + "create-new": "새로 만들기", + "filter-required": "필터를 입력하세요.", + "operation": { + "operation": "연산", + "equal": "같음", + "not-equal": "같이 않음", + "starts-with": "다음으로 시작함", + "ends-with": "다음으로 끝남", + "contains": "포함함", + "not-contains": "포함하지 않음", + "greater": "큼", + "less": "작음", + "greater-or-equal": "크거나 같음", + "less-or-equal": "작거나 같음", + "and": "그리고", + "or": "또는", + "in": "포함", + "not-in": "미포함" + }, + "ignore-case": "대소문자 무시", + "value": "값", + "remove-filter": "필터 제거", + "duplicate-filter-action": "필터 복제", + "preview": "필터 미리보기", + "no-filters": "구성된 필터 없음", + "add-filter": "필터 추가", + "add-complex-filter": "복합 필터 추가", + "add-complex": "복합 추가", + "complex-filter": "복합 필터", + "edit-complex-filter": "복합 필터 편집", + "edit-filter-user-params": "필터 술어 사용자 매개변수 편집", + "filter-user-params": "필터 술어 사용자 매개변수", + "user-parameters": "사용자 매개변수", + "display-label": "표시할 레이블", + "custom-label": "사용자 지정 레이블", + "custom-label-hint": "필터에 사용자 지정 레이블을 설정하려면 활성화합니다. 비활성화하면 레이블이 자동으로 생성됩니다.", + "order-priority": "표시 순서", + "key-filter": "키 필터", + "key-filters": "키 필터", + "key-name": "키 이름", + "key-name-required": "키 이름은 필수 항목입니다.", + "key-type": { + "key-type": "키 유형", + "attribute": "속성", + "timeseries": "시계열", + "entity-field": "엔티티 필드", + "constant": "상수", + "client-attribute": "클라이언트 속성", + "server-attribute": "서버 속성", + "shared-attribute": "공유된 속성" + }, + "value-type": { + "value-type": "값 유형", + "string": "문자열", + "numeric": "숫자", + "boolean": "불리언", + "date-time": "날짜/시간" + }, + "value-type-required": "키 값 유형은 필수 항목입니다.", + "key-value-type-change-title": "키 값 유형을 변경하시겠습니까?", + "key-value-type-change-message": "확인하면 입력한 모든 키 필터가 제거됩니다.", + "no-key-filters": "구성된 키 필터 없음", + "add-key-filter": "키 필터 추가", + "remove-key-filter": "키 필터 제거", + "edit-key-filter": "키 필터 편집", + "date": "날짜", + "time": "시간", + "current-tenant": "현재 테넌트", + "current-customer": "현재 고객", + "current-user": "현재 사용자", + "current-device": "현재 장치", + "default-value": "기본값", + "default-comma-separated-values": "기본 쉼표로 구분된 값", + "dynamic-source-type": "동적 소스 유형", + "dynamic-value": "동적 값", + "no-dynamic-value": "동적 값 없음", + "source-attribute": "소스 속성", + "switch-to-dynamic-value": "동적 값으로 전환", + "switch-to-default-value": "기본값으로 전환", + "inherit-owner": "소유자로부터 상속", + "source-attribute-not-set": "소스 속성이 설정되지 않은 경우", + "unit": "단위" }, "fullscreen": { "expand": "전체화면으로 확장", @@ -1583,15 +3664,14 @@ "function": "기능" }, "gateway": { - "create-new-gateway": "Create a new gateway", - "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", - "gateway": "Gateway", - "gateway-exists": "Device with same name is already exists.", - "gateway-name": "Gateway name", - "gateway-name-required": "Gateway name is required.", - "gateway-saved": "Gateway configuration successfully saved.", - "no-gateway-found": "No gateway found.", - "no-gateway-matching": " '{{item}}' not found." + "gateway-name": "게이트웨이 이름", + "gateway-name-required": "게이트웨이 이름은 필수 항목입니다.", + "gateways": "게이트웨이", + "create-new-gateway": "새 게이트웨이 생성", + "create-new-gateway-text": "이름이 '{{gatewayName}}'인 새 게이트웨이를 생성하시겠습니까?", + "launch-command": "실행 명령", + "no-gateway-found": "게이트웨이를 찾을 수 없습니다.", + "no-gateway-matching": " '{{item}}'을(를) 찾을 수 없습니다." }, "grid": { "delete-item-title": "이 항목을 삭제 하시겠습니까?", @@ -1607,28 +3687,98 @@ "scroll-to-top": "스크롤 맨 위로" }, "help": { - "goto-help-page": "도움" + "goto-help-page": "도움", + "show-help": "도움말 표시" }, "home": { "home": "홈", - "profile": "프로파일", + "profile": "프로필", "logout": "로그아웃", "menu": "메뉴", - "avatar": "Avatar", + "avatar": "아바타", "open-user-menu": "사용자 메뉴 열기" }, + "file-input": { + "browse-file": "파일 찾아보기", + "browse-files": "파일 찾아보기" + }, + "image": { + "gallery": "이미지 갤러리", + "search": "이미지 검색", + "selected-images": "{ count, plural, =1 {1개 이미지} other {#개 이미지} } 선택됨", + "created-time": "생성 일시", + "name": "이름", + "name-required": "이름을 입력하세요.", + "resolution": "해상도", + "size": "크기", + "system": "시스템", + "download-image": "이미지 다운로드", + "export-image": "이미지를 JSON으로 내보내기", + "import-image": "JSON에서 이미지 가져오기", + "upload-image": "이미지 업로드", + "edit-image": "이미지 수정", + "image-details": "이미지 상세", + "no-images": "이미지가 없습니다", + "delete-image": "이미지 삭제", + "delete-image-title": "이미지 '{{imageTitle}}'을(를) 삭제하시겠습니까?", + "delete-image-text": "주의: 확인 후 이미지를 복구할 수 없게 됩니다.", + "delete-images-title": "{ count, plural, =1 {1개 이미지} other {#개 이미지} }를 삭제하시겠습니까?", + "delete-images-text": "주의: 확인 후 선택한 모든 이미지가 제거되고 모든 관련 데이터를 복구할 수 없게 됩니다.", + "list-mode": "목록 보기", + "grid-mode": "그리드 보기", + "image-preview": "이미지 미리보기", + "update-image": "이미지 업데이트", + "export-failed-error": "이미지를 내보낼 수 없습니다: {{error}}", + "image-json-file": "이미지 JSON 파일", + "invalid-image-json-file-error": "JSON에서 이미지를 가져올 수 없습니다: 잘못된 이미지 JSON 데이터 구조입니다.", + "image-is-in-use": "이미지가 다른 엔티티에서 사용 중입니다", + "images-are-in-use": "이미지가 다른 엔티티에서 사용 중입니다", + "image-is-in-use-text": "'{{title}}' 이미지가 다음 엔티티에서 사용 중이므로 삭제되지 않았습니다:", + "images-are-in-use-text": "모든 이미지가 삭제되지 않았습니다. 다른 엔티티에서 사용 중이기 때문입니다.
해당 이미지 행의 참조 버튼을 클릭하여 참조된 엔티티를 볼 수 있습니다.
이 이미지를 계속 삭제하려면 아래 표에서 선택하고 선택 삭제 버튼을 클릭하세요.", + "delete-image-in-use-text": "이미지를 계속 삭제하려면 무조건 삭제 버튼을 클릭하세요.", + "system-entities": "시스템 엔티티:", + "entities": "엔티티:", + "references": "참조", + "include-system-images": "시스템 이미지 포함", + "clear-image": "이미지 지우기", + "no-image": "이미지 없음", + "no-image-selected": "이미지 없음", + "browse-from-gallery": "갤러리에서 찾아보기", + "set-link": "링크 설정", + "image-link": "이미지 링크", + "link": "링크", + "copy-image-link": "이미지 링크 복사", + "embed-image": "이미지 포함", + "embed-to-html": "HTML에 포함", + "embed-to-html-hint": "이 기능은 승인되지 않은 사용자도 링크에 액세스할 수 있게 합니다.", + "embed-to-html-text": "다음 코드 스니펫을 사용하여 일반 HTML 기반 컴포넌트에 이미지를 포함할 수 있습니다.
이러한 컴포넌트에는 HTML 카드 위젯, 셀 콘텐츠 함수 등이 포함됩니다.", + "embed-to-angular-template": "Angular HTML 템플릿에 포함", + "embed-to-angular-template-text": "다음 코드 스니펫을 사용하여 컴포넌트에 사용될 Angular HTML 템플릿에 이미지를 포함할 수 있습니다.
이러한 컴포넌트에는 마크다운 위젯, 위젯 편집기의 HTML 섹션, 사용자 지정 작업 등이 포함됩니다." + }, + "image-input": { + "drop-images-or": "이미지를 끌어다 놓거나", + "drag-and-drop": "끌어다 놓기", + "or": "또는", + "browse": "찾아보기", + "no-images": "선택된 이미지 없음", + "images": "이미지" + }, "import": { "no-file": "선택된 파일이 없습니다.", "drop-file": "업로드 할 JSON 파일을 끌어다 놓거나 여기 클릭하여 파일을 선택하십시오.", + "drop-json-file-or": "JSON 파일을 끌어다 놓거나", "drop-file-csv": "업로드 할 CSV 파일을 끌어다 놓거나 여기 클릭하여 파일을 선택하십시오.", + "drop-file-csv-or": "CSV 파일을 끌어다 놓거나", "column-value": "값", "column-title": "제목", "column-example": "값 데이터 예시", "column-key": "속성/텔레메트리 키", - "csv-delimiter": "CSV delimiter", + "credentials": "자격 증명", + "csv-delimiter": "CSV 구분자", "csv-first-line-header": "첫번째 줄에 컬럼 이름 포함", "csv-update-data": "속성/텔레메트리 업데이트", - "import-csv-number-columns-error": "A file should contain at least two columns", + "details": "상세 내역", + "import-csv-number-columns-error": "파일에는 최소 두 개의 열이 포함되어야 합니다.", "import-csv-invalid-format-error": "유효하지 않은 파일 포맷. Line: '{{line}}'", "column-type": { "name": "이름", @@ -1639,21 +3789,388 @@ "shared-attribute": "공유된 속성", "server-attribute": "서버 속성", "timeseries": "시계열", - "entity-field": "개체 필드", + "entity-field": "엔티티 필드", "access-token": "액세스 토큰", + "x509": "X.509", + "mqtt": { + "client-id": "MQTT 클라이언트 ID", + "user-name": "MQTT 사용자 이름", + "password": "MQTT 비밀번호" + }, + "lwm2m": { + "client-endpoint": "LwM2M 엔드포인트 클라이언트 이름", + "security-config-mode": "LwM2M 보안 구성 모드", + "client-identity": "LwM2M 클라이언트 ID", + "client-key": "LwM2M 클라이언트 키", + "client-cert": "LwM2M 클라이언트 공개 키", + "bootstrap-server-security-mode": "LwM2M 부트스트랩 서버 보안 모드", + "bootstrap-server-secret-key": "LwM2M 부트스트랩 서버 비밀 키", + "bootstrap-server-public-key-id": "LwM2M 부트스트랩 서버 공개 키 또는 ID", + "lwm2m-server-security-mode": "LwM2M 서버 보안 모드", + "lwm2m-server-secret-key": "LwM2M 서버 비밀 키", + "lwm2m-server-public-key-id": "LwM2M 서버 공개 키 또는 ID" + }, + "snmp": { + "host": "SNMP 호스트", + "port": "SNMP 포트", + "version": "SNMP 버전 (v1, v2c 또는 v3)", + "community-string": "SNMP 커뮤니티 문자열" + }, "isgateway": "게이트웨이 여부", - "description": "설정" + "activity-time-from-gateway-device": "게이트웨이 장치의 활동 시간", + "description": "설정", + "routing-key": "엣지 키", + "secret": "엣지 시크릿" }, - "stepper-text":{ + "stepper-text": { "select-file": "파일 선택", "configuration": "설정 불러오기", "column-type": "컬럼 유형 선택", - "creat-entities": "새로운 개체 생성" + "creat-entities": "새로운 엔티티 생성" }, "message": { - "create-entities": "{{count}}개의 새로운 개체사 성공적으로 생성되었습니다.", - "update-entities": "{{count}}개의 개체가 성공적으로 업데이트 되었습니다.", - "error-entities": "{{count}}개의 개체를 생성하는데 오류가 발생했습니다." + "create-entities": "{{count}}개의 새로운 엔티티사 성공적으로 생성되었습니다.", + "update-entities": "{{count}}개의 엔티티가 성공적으로 업데이트 되었습니다.", + "error-entities": "{{count}}개의 엔티티를 생성하는데 오류가 발생했습니다." + } + }, + "scada": { + "symbols": "SCADA 심볼", + "search": "심볼 검색", + "selected-symbols": "{ count, plural, =1 {1개 심볼} other {#개 심볼} } 선택됨", + "download-symbol": "SCADA 심볼 다운로드", + "export-symbol": "SCADA 심볼을 JSON으로 내보내기", + "import-symbol": "JSON에서 SCADA 심볼 가져오기", + "upload-symbol": "SCADA 심볼 업로드", + "update-symbol": "SCADA 심볼 업데이트", + "edit-symbol": "SCADA 심볼 수정", + "symbol-details": "SCADA 심볼 상세", + "mode-svg": "SVG", + "mode-xml": "XML", + "no-symbols": "심볼이 없습니다", + "show-hidden-elements": "숨겨진 요소 표시", + "hide-hidden-elements": "숨겨진 요소 숨기기", + "delete-symbol": "SCADA 심볼 삭제", + "delete-symbol-title": "SCADA 심볼 '{{imageTitle}}'을(를) 삭제하시겠습니까?", + "delete-symbol-text": "주의: 확인 후 SCADA 심볼을 복구할 수 없게 됩니다.", + "delete-symbols-title": "{ count, plural, =1 {1개 SCADA 심볼} other {#개 SCADA 심볼} }을(를) 삭제하시겠습니까?", + "delete-symbols-text": "주의: 확인 후 선택한 모든 SCADA 심볼이 제거되고 모든 관련 데이터를 복구할 수 없게 됩니다.", + "include-system-symbols": "시스템 심볼 포함", + "symbol-preview": "심볼 미리보기", + "general": "일반", + "tags": "태그", + "properties": "속성", + "title": "제목", + "description": "설명", + "search-tags": "태그 검색", + "widget-size": "위젯 크기", + "cols": "열", + "rows": "행", + "state-render-function": "상태 렌더링 함수", + "preview": "미리보기", + "preview-widget-action-text": "위젯 액션 '{{type}}'이(가) 성공적으로 호출되었습니다!", + "no-symbol": "SCADA 심볼 없음", + "no-symbol-selected": "선택된 SCADA 심볼 없음", + "clear-symbol": "SCADA 심볼 지우기", + "browse-symbol-from-gallery": "갤러리에서 SCADA 심볼 찾아보기", + "zoom-in": "확대", + "zoom-out": "축소", + "create-widget": "위젯 만들기", + "create-widget-from-symbol": "SCADA 심볼로 위젯 만들기", + "hidden": "숨겨짐", + "tag": { + "tag": "태그", + "on-click-action": "클릭 시 액션", + "no-tags": "구성된 태그 없음", + "delete-tag-text": "{{elementType}} 요소에서 태그
{{tag}}을(를) 삭제하시겠습니까?", + "update-tag": "태그 업데이트", + "enter-tag": "태그 입력", + "tag-settings": "태그 설정", + "remove-tag": "태그 제거", + "add-tag": "태그 추가" + }, + "behavior": { + "behavior": "동작", + "id": "ID", + "name": "이름", + "type": "유형", + "no-behaviors": "구성된 동작 없음", + "add-behavior": "동작 추가", + "type-action": "동작", + "type-value": "값", + "type-widget-action": "위젯 액션", + "behavior-settings": "동작 설정", + "remove-behavior": "동작 제거", + "hint": "힌트", + "group-title": "그룹 제목", + "value-type": "값 유형", + "default-value": "기본값", + "true-label": "참 라벨", + "false-label": "거짓 라벨", + "state-label": "상태 라벨", + "default-payload": "기본 페이로드", + "not-unique-behavior-ids-error": "동작 ID는 고유해야 합니다!", + "default-settings": "기본 설정" + }, + "symbol": { + "symbol": "SCADA 심볼", + "fluid-presence": "유체 존재", + "fluid-presence-hint": "파이프에 유체가 있는지 나타냅니다.", + "fluid-present": "유체 있음", + "present": "있음", + "absent": "없음", + "flow-presence": "흐름 존재", + "flow-presence-hint": "파이프에서 유체가 흐르고 있는지 나타냅니다.", + "flow-present": "흐름 있음", + "flow-direction": "흐름 방향", + "flow-direction-hint": "유체 흐름 방향을 나타냅니다.", + "forward": "전방", + "reverse": "역방향", + "flow-animation-speed": "흐름 애니메이션 속도", + "flow-animation-speed-hint": "흐름의 애니메이션 속도를 나타내는 double 값. 1 - 정상 속도, 0 - 애니메이션 없음, < 1 - 느린 애니메이션, > 1 - 빠른 애니메이션.", + "leak": "누출", + "leak-hint": "파이프에 누출이 있는지 나타냅니다.", + "leak-present": "누출 있음", + "fluid-color": "유체 색상", + "pipe-color": "파이프 색상", + "horizontal-pipe": "수평 파이프", + "vertical-pipe": "수직 파이프", + "horizontal-fluid-color": "수평 유체 색상", + "vertical-fluid-color": "수직 유체 색상", + "left-pipe": "왼쪽 파이프", + "right-pipe": "오른쪽 파이프", + "top-pipe": "상단 파이프", + "bottom-pipe": "하단 파이프", + "left-fluid-color": "왼쪽 유체 색상", + "right-fluid-color": "오른쪽 유체 색상", + "top-fluid-color": "상단 유체 색상", + "bottom-fluid-color": "하단 유체 색상", + "display": "디스플레이", + "display-format": "디스플레이 형식", + "value": "값", + "decimals": "소수 자릿수", + "units": "단위", + "flow-meter-value-hint": "유량계 디스플레이에 표시되는 double 값", + "value-hint": "현재 값을 나타내는 double 값", + "running": "실행 중", + "running-hint": "구성 요소가 실행 상태인지 나타냅니다.", + "warning-state": "경고 상태", + "warning": "경고", + "warning-click": "경고 클릭", + "warning-state-hint": "구성 요소가 경고 상태인지 나타냅니다.", + "critical-state": "심각 상태", + "critical": "심각한", + "critical-click": "중요 클릭", + "critical-state-hint": "구성 요소가 심각 상태인지 나타냅니다.", + "critical-state-animation": "심각 상태 애니메이션", + "critical-state-animation-hint": "구성 요소가 심각 상태일 때 깜박이는 애니메이션을 활성화할지 여부.", + "warning-critical-state-animation": "경고/심각 상태 애니메이션", + "warning-critical-state-animation-hint": "구성 요소가 경고 또는 심각 상태일 때 깜박이는 애니메이션을 활성화할지 여부.", + "animation": "애니메이션", + "broken": "손상됨", + "broken-hint": "구성 요소가 손상되었는지 나타냅니다.", + "on-display-click": "디스플레이 클릭 시", + "on-display-click-hint": "사용자가 디스플레이를 클릭할 때 호출되는 액션.", + "pipe": "파이프", + "default-border-color": "기본 테두리 색상", + "active-border-color": "활성 테두리 색상", + "warning-border-color": "경고 테두리 색상", + "critical-border-color": "심각 테두리 색상", + "background-color": "배경색", + "rotation-animation-speed": "회전 애니메이션 속도", + "rotation-animation-speed-hint": "회전 애니메이션 속도를 나타내는 double 값. 1 - 정상 속도, 0 - 애니메이션 없음, < 1 - 느린 애니메이션, > 1 - 빠른 애니메이션.", + "on-click": "클릭 시", + "on-click-hint": "사용자가 구성 요소를 클릭할 때 호출되는 액션.", + "connectors-positions": "커넥터 위치", + "right-connector": "오른쪽 커넥터", + "right-top-connector": "오른쪽 상단 커넥터", + "right-bottom-connector": "오른쪽 하단 커넥터", + "left-connector": "왼쪽 커넥터", + "left-top-connector": "왼쪽 상단 커넥터", + "left-bottom-connector": "왼쪽 하단 커넥터", + "top-left-connector": "상단 왼쪽 커넥터", + "top-right-connector": "상단 오른쪽 커넥터", + "top-connector": "상단 커넥터", + "bottom-connector": "하단 커넥터", + "running-color": "실행 색상", + "stopped-color": "정지 색상", + "stopped": "정지됨", + "warning-color": "경고 색상", + "critical-color": "심각 색상", + "opened": "열림", + "opened-hint": "구성 요소가 열린 상태인지 나타냅니다.", + "open": "열기", + "open-hint": "사용자가 구성 요소를 열기 위해 클릭할 때 호출되는 액션.", + "close": "닫기", + "close-hint": "사용자가 구성 요소를 닫기 위해 클릭할 때 호출되는 액션.", + "close-state-animation": "닫힘 상태 애니메이션", + "close-state-animation-hint": "구성 요소가 닫힌 상태일 때 깜박이는 애니메이션을 활성화할지 여부.", + "opened-color": "열림 색상", + "closed-color": "닫힘 색상", + "opened-rotation-angle": "열림 회전 각도", + "closed-rotation-angle": "닫힘 회전 각도", + "tank-capacity": "탱크 용량", + "tank-capacity-hint": "총 탱크 용량을 나타내는 double 값.", + "current-volume": "현재 볼륨", + "current-volume-hint": "현재 사용 중인 볼륨을 나타내는 double 값.", + "tank-color": "탱크 색상", + "value-box": "값 상자", + "value-text": "값 텍스트", + "scale": "눈금", + "transparent-mode": "투명 모드", + "major-ticks": "주 눈금", + "intervals": "간격", + "major-ticks-color": "주 눈금 색상", + "normal": "정상", + "minor-ticks": "보조 눈금", + "minor-ticks-color": "보조 눈금 색상", + "temperature": "온도", + "temperature-hint": "현재 온도를 나타내는 double 값.", + "update-temperature": "온도 업데이트", + "update-temperature-hint": "사용자가 현재 온도를 변경하기 위해 클릭할 때 호출되는 액션.", + "run": "실행", + "run-hint": "사용자가 구성 요소를 실행하기 위해 클릭할 때 호출되는 액션.", + "stop": "정지", + "stop-hint": "사용자가 구성 요소를 정지하기 위해 클릭할 때 호출되는 액션.", + "temperature-step": "온도 단계 증가", + "heat-pump-color": "히트 펌프 색상", + "power-button-background": "전원 버튼 배경", + "value-box-background": "값 상자 배경", + "value-units": "값 단위", + "enable-units-scale": "눈금에 단위 표시", + "filtration-mode": "필터 모드", + "filtration-mode-hint": "현재 필터 모드를 나타내는 정수 값.", + "filtration-mode-update": "필터 모드 업데이트 상태", + "filtration-mode-update-hint": "사용자가 현재 필터 모드를 변경하기 위해 클릭할 때 호출되는 동작.", + "filter-mode": "필터", + "waste-mode": "폐기", + "backwash-mode": "역세척", + "recirculate-mode": "순환", + "rinse-mode": "헹굼", + "closed-mode": "닫힘", + "sand-filter-color": "모래 필터 색상", + "mode-box-background": "모드 상자 배경", + "border-color": "테두리 색상", + "label-color": "레이블 색상", + "water-leak-hint": "누출 여부를 나타냅니다.", + "default-color": "기본 색상", + "leak-color": "누출 색상", + "full-value": "전체 값", + "full-value-hint": "전체 값을 나타내는 실수 값.", + "label": "라벨", + "icon": "아이콘", + "button-color": "버튼 색상", + "on-label": "'켜짐' 레이블 텍스트", + "off-label": "'꺼짐' 레이블 텍스트", + "arrow-presence": "화살표 표시", + "arrow-presence-hint": "연결기에 화살표가 있는지 나타냅니다.", + "arrow-present": "화살표 있음", + "arrow-direction": "흐름 방향", + "arrow-direction-hint": "흐름 방향을 나타냅니다.", + "flow-animation": "흐름 존재", + "flow-animation-hint": "연결기에 유체가 흐르고 있는지 나타냅니다.", + "flow": "흐름", + "flow-line": "선", + "flow-line-style": "선 스타일", + "flow-style-hint": "완벽한 애니메이션 동기화를 위해 Dash와 Gap 값의 합이 100으로 나누어 떨어지도록 설정하세요.", + "flow-dash-cap": "대시 끝", + "dash-cap-butt": "평면", + "dash-cap-round": "둥글게", + "dash-cap-square": "사각형", + "dash": "대시", + "gap": "간격", + "main-line": "메인 선", + "line": "선", + "line-color": "선 색상", + "arrow-color": "화살표 색상", + "target-value": "목표 값", + "target-value-hint": "눈금의 목표 지점을 나타냅니다.", + "min-max-value": "최소 및 최대 값", + "min-value": "최소", + "max-value": "최대", + "progress-bar": "진행 막대", + "progress-arrow": "진행 화살표", + "warning-scale-color": "경고 눈금 색상", + "critical-scale-color": "중요 눈금 색상", + "scale-color": "스케일 색상", + "target": "대상", + "high-warning-state": "높은 경고 상태", + "show-high-warning-scale": "높은 경고 스케일 표시", + "high-warning-scale": "높은 경고 스케일", + "high-warning-state-hint": "double 값은 높은 경고 범위가 높은 위험 또는 최대값까지임을 나타냅니다.", + "low-warning-state": "낮은 경고 상태", + "show-low-warning-scale": "낮은 경고 스케일 표시", + "low-warning-scale": "낮은 경고 스케일", + "low-warning-state-hint": "double 값은 낮은 경고 범위가 낮은 위험 또는 최소값까지임을 나타냅니다.", + "high-critical-state": "높은 위험 상태", + "show-high-critical-scale": "높은 위험 스케일 표시", + "high-critical-scale": "높은 위험 스케일", + "high-critical-state-hint": "double 값은 높은 위험 범위가 최대값 스케일까지임을 나타냅니다.", + "low-critical-state": "낮은 위험 상태", + "show-low-critical-scale": "낮은 위험 상태 표시", + "low-critical-scale": "낮은 위험 상태", + "low-critical-state-hint": "double 값은 낮은 위험 범위가 최소값 스케일까지임을 나타냅니다.", + "filter-color": "필터 색상", + "colors": "색상", + "indicator-colors": "표시기 색상", + "enabled": "사용", + "disabled": "비활성화됨", + "on": "켜짐", + "off": "꺼짐", + "on-off-state": "켜짐/꺼짐 상태", + "on-off-state-hint": "컴포넌트가 켜짐 또는 꺼짐 상태인지 나타냅니다.", + "on-update-state": "켜짐 업데이트 상태", + "on-update-state-hint": "사용자가 상태를 켜짐으로 업데이트하기 위해 클릭할 때 호출되는 작업입니다.", + "off-update-state": "꺼짐 업데이트 상태", + "off-update-state-hint": "사용자가 상태를 꺼짐으로 업데이트하기 위해 클릭할 때 호출되는 작업입니다.", + "voltage": "전압", + "input-voltage": "입력 전압", + "input-voltage-hint": "double 값은 입력 전압 값을 나타냅니다.", + "output-voltage": "출력 전압", + "output-voltage-hint": "출력 전압 값을 나타내는 실수 값.", + "first-phase-voltage": "1상 전압", + "second-phase-voltage": "2상 전압", + "third-phase-voltage": "3상 전압", + "phase-voltage-hint": "현재 위상의 전압 값을 나타내는 실수 값", + "voltage-hint": "현재 전압을 나타내는 실수 값", + "current-voltage-color": "현재 전압 색상", + "phase-indicator-color": "위상 표시기 색상", + "measured": "측정됨", + "measured-hint": "킬로와트시 단위의 에너지 사용량을 나타내는 실수 값", + "day-rate": "주간 요금", + "night-rate": "야간 요금", + "off-peak-rate": "비성수기 요금", + "peak-rate": "성수기 요금", + "export-rate": "수출 요금", + "operating-mode": "운영 모드", + "bypass-mode": "바이패스", + "operating-mode-hint": "현재 작동 모드를 나타내는 정수 값 (0 - 꺼짐, 1 - 켜짐, 2 - 바이패스)", + "connected": "연결됨", + "connected-hint": "구성 요소가 연결 상태인지 나타냅니다.", + "disconnected": "연결 해제됨", + "indicator": "표시기", + "operation-mode": "운영 모드", + "operation-mode-hint": "인버터가 메인 또는 인버터 모드인지 나타냅니다.", + "operation-mode-indicators-color": "운영 모드 표시기 색상", + "mains-on-mode": "메인 켜짐", + "inverter-on-mode": "인버터 켜짐", + "charging-mode": "충전 모드", + "charging-mode-hint": "현재 충전 모드를 나타내는 정수 값 (1 - 벌크, 2 - 흡수, 3 - 플로트)", + "charging-mode-indicators-color": "충전 모드 표시기 색상", + "inverter-faults": "고장", + "inverter-fault-indicators-color": "고장 표시기 색상", + "overload-fault": "과부하", + "overload-fault-hint": "인버터가 과부하 상태인지 나타냅니다.", + "low-battery-fault": "배터리 부족", + "low-battery-fault-hint": "배터리가 과도하게 방전되었는지 나타냅니다.", + "temperature-fault": "온도", + "temperature-fault-hint": "인버터에 고온이 있는지 나타냅니다.", + "triangle": "삼각형", + "socket": "소켓", + "left-button": "왼쪽 버튼", + "right-button": "오른쪽 버튼", + "alarm-colors": "알람 색상", + "hook-color": "후크 색상" } }, "item": { @@ -1662,8 +4179,25 @@ "js-func": { "no-return-error": "함수는 값을 반환해야 합니다!", "return-type-mismatch": "함수는 '{{type}}' 유형의 값을 반환해야 합니다!", - "tidy": "Tidy", - "mini": "Mini" + "tidy": "깔끔한", + "mini": "미니", + "modules": "모듈", + "remove-module": "모듈 제거", + "no-modules": "구성된 모듈 없음", + "add-module": "모듈 추가", + "module-alias": "별명", + "invalid-module-alias-name": "잘못된 별명 이름", + "module-resource": "JS 모듈 리소스", + "not-unique-module-aliases-error": "모듈 별명은 고유해야 합니다!", + "show-module-info": "모듈 정보 표시", + "show-module-source-code": "모듈 소스 코드 표시", + "module-members": "모듈 멤버", + "module-no-members": "모듈에 내보낸 멤버가 없습니다", + "module-load-error": "모듈 로드 오류", + "source-code": "소스 코드", + "source-code-load-error": "소스 코드 로드 오류", + "no-js-module-text": "JS 모듈을 찾을 수 없음", + "no-js-module-matching": "'{{module}}'와(과) 일치하는 JS 모듈을 찾을 수 없습니다." }, "key-val": { "key": "키", @@ -1674,52 +4208,660 @@ }, "layout": { "layout": "레이아웃", + "layouts": "레이아웃", "manage": "레이아웃 관리", "settings": "레이아웃 설정", "color": "색", - "main": "Main", + "main": "메인", "right": "오른쪽", - "select": "대상 레이아웃 선택" + "left": "왼쪽", + "select": "대상 레이아웃 선택", + "percentage-width": "백분율 너비 (%)", + "fixed-width": "고정 너비 (px)", + "left-width": "왼쪽 열 (%)", + "right-width": "오른쪽 열 (%)", + "pick-fixed-side": "고정 측면: ", + "layout-fixed-width": "고정 너비 (px)", + "value-min-error": "값은 {{min}}{{unit}}보다 커야 합니다.", + "value-max-error": "값은 {{max}}{{unit}}보다 작아야 합니다.", + "layout-fixed-width-required": "고정 너비는 필수 항목입니다.", + "right-width-percentage-required": "오른쪽 백분율은 필수 항목입니다.", + "left-width-percentage-required": "왼쪽 백분율은 필수 항목입니다.", + "divider": "구분선", + "right-side": "오른쪽 레이아웃", + "left-side": "왼쪽 레이아웃", + "add-new-breakpoint": "새 브레이크포인트 추가", + "breakpoint": "브레이크포인트", + "breakpoints": "브레이크포인트", + "copy-from": "복사 원본", + "size": "크기", + "delete-breakpoint-title": "브레이크포인트 '{{name}}'을(를) 삭제하시겠습니까?", + "delete-breakpoint-text": "참고: 확인 후 중단점을 복구할 수 없게 되며 설정이 기본 중단점으로 되돌아갑니다." }, "legend": { "direction": "범례 방향", "position": "범례 위치", - "sort-legend": "범례에 데이터키 정렬", + "show-values": "값 표시", + "min-option": "최소", + "max-option": "최대", + "average-option": "평균", + "total-option": "합계", + "latest-option": "최신", + "sort-legend": "범례에 데이터키 정렬", "show-max": "최대값 표시", "show-min": "최소값 표시", "show-avg": "평균값 표시", "show-total": "총합 표시", + "show-latest": "최신 값 표시", "settings": "범례 설정", "min": "최소", "max": "최대", "avg": "평균", "total": "합계", + "latest": "최신", + "Min": "최소", + "Max": "최대", + "Avg": "평균", + "Total": "합계", + "Latest": "최신", "comparison-time-ago": { + "previousInterval": "(이전 간격)", + "customInterval": "(사용자 지정 간격)", "days": "(일 전)", "weeks": "(주 전)", "months": "(달 전)", "years": "(년 전)" - } + }, + "column-title": "열 제목", + "label": "라벨", + "value": "값" }, "login": { "login": "로그인", "request-password-reset": "비밀번호 재설정", "reset-password": "비밀번호 재설정", "create-password": "비밀번호 생성", + "two-factor-authentication": "2단계 인증", "passwords-mismatch-error": "입력된 비밀번호는 같아야 합니다!", "password-again": "비밀번호 확인", + "sign-in": "로그인", "username": "사용자명 (이메일)", "remember-me": "아이디 저장", "forgot-password": "비밀번호찾기", "password-reset": "비밀번호 재설정", - "expired-password-reset-message": "당신의 크리덴셜이 만료되었습니다! 새로운 비밀번호를 생성하세요.", + "expired-password-reset-message": "당신의 자격 증명이 만료되었습니다! 새로운 비밀번호를 생성하세요.", "new-password": "새 비밀번호", "new-password-again": "새 비밀번호 확인", "password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!", "email": "이메일", - "login-with": "{{name}}으로 로그인", + "invalid-email-format": "잘못된 Email 형식입니다.", + "sign-in-with": "{{name}}(으)로 로그인", + "sign-in-to-your-account": "계정에 로그인하세요", "or": "또는", - "error": "로그인 오류" + "error": "로그인 오류", + "verify-your-identity": "신원 확인", + "select-way-to-verify": "확인 방법 선택", + "resend-code": "코드 재전송", + "resend-code-wait": "{ time, plural, =1 {1초} other {#초} } 후에 코드 재전송", + "try-another-way": "다른 방법 시도", + "totp-auth-description": "인증기 앱에서 보안 코드를 입력하세요.", + "totp-auth-placeholder": "코드", + "sms-auth-description": "보안 코드가 {{contact}}의 전화번호로 전송되었습니다.", + "sms-auth-placeholder": "SMS 코드", + "email-auth-description": "보안 코드가 {{contact}}의 이메일 주소로 전송되었습니다.", + "email-auth-placeholder": "이메일 코드", + "backup-code-auth-description": "백업 코드 중 하나를 입력하세요.", + "backup-code-auth-placeholder": "백업 코드", + "activation-link-expired": "활성화 링크가 만료되었습니다", + "activation-link-expired-message": "프로필 활성화 링크가 만료되었습니다. 로그인 페이지로 돌아가서 새 이메일을 받을 수 있습니다.", + "reset-password-link-expired": "비밀번호 재설정 링크가 만료되었습니다", + "reset-password-link-expired-message": "비밀번호 재설정 링크가 만료되었습니다. 로그인 페이지로 돌아가서 새 이메일을 받을 수 있습니다.", + "two-fa": "2단계 인증", + "two-fa-required": "2단계 인증이 필요합니다", + "set-up-verification-method": "계속하려면 확인 방법을 설정하세요", + "set-up-verification-method-login": "확인 방법을 설정하거나 로그인하세요", + "enable-authenticator-app": "인증기 앱 활성화", + "enable-authenticator-app-description": "인증기 앱에서 보안 코드를 입력하세요", + "enable-authenticator-sms": "SMS 인증기 활성화", + "enable-authenticator-sms-description": "방금 전송된 6자리 코드를 입력하세요: ", + "enable-authenticator-email": "이메일 인증기 활성화", + "enable-authenticator-email-description": "보안 코드가 이메일 주소로 전송되었습니다: ", + "enter-key-manually": "또는 이 32자리 키를 직접 입력하세요:", + "continue": "계속", + "confirm": "확인", + "authenticator-app-success": "인증기 앱이 활성화되었습니다", + "authenticator-app-success-description": "다음에 로그인할 때 2단계 인증 코드를 제공해야 합니다", + "authenticator-sms-success": "SMS 인증기가 활성화되었습니다", + "authenticator-sms-success-description": "다음에 로그인할 때 전화번호로 전송될 보안 코드를 입력하라는 메시지가 표시됩니다", + "authenticator-email-success": "이메일 인증기가 활성화되었습니다", + "authenticator-email-success-description": "다음에 로그인할 때 이메일 주소로 전송될 보안 코드를 입력하라는 메시지가 표시됩니다", + "authenticator-backup-code-success": "백업 코드가 활성화되었습니다", + "authenticator-backup-code-success-description": "다음에 로그인할 때 보안 코드를 입력하거나 백업 코드 중 하나를 사용하라는 메시지가 표시됩니다.", + "add-verification-method": "확인 방법 추가", + "get-backup-code": "백업 코드 받기", + "copy-key": "키 복사", + "send-code": "코드 전송", + "email-label": "이메일", + "email-description": "인증기로 사용할 이메일을 입력하세요.", + "sms-description": "인증기로 사용할 전화번호를 입력하세요.", + "backup-code-description": "코드를 인쇄하여 계정에 로그인할 때 필요할 때 사용할 수 있도록 하세요. 각 백업 코드는 한 번만 사용할 수 있습니다.", + "backup-code-warn": "이 페이지를 떠나면 이 코드를 다시 볼 수 없습니다. 아래 옵션을 사용하여 안전하게 보관하세요.", + "download-txt": "다운로드 (txt)", + "print": "인쇄", + "verification-code": "6자리 코드", + "verification-code-invalid": "잘못된 확인 코드 형식입니다.", + "verification-code-incorrect": "확인 코드가 올바르지 않습니다.", + "verification-code-many-request": "확인 코드 확인 요청이 너무 많습니다.", + "scan-qr-code": "인증 앱으로 이 QR 코드를 스캔하세요", + "phone-input": { + "phone-input-label": "전화번호", + "phone-input-required": "전화번호는 필수 항목입니다.", + "phone-input-validation": "전화번호가 유효하지 않거나 불가능합니다.", + "phone-input-pattern": "잘못된 전화번호입니다. E.164 형식을 사용하세요 (예: {{phoneNumber}})", + "phone-input-hint": "E.164 형식의 전화번호 (예: {{phoneNumber}})" + } + }, + "markdown": { + "edit": "수정", + "preview": "미리보기", + "copy-code": "클릭하여 복사", + "copied": "복사됨!" + }, + "mobile": { + "add-application": "애플리케이션 추가", + "app-id": "앱 사이트 연결 ID", + "app-id-required": "앱 사이트 연결 ID가 필요합니다", + "app-id-pattern": "잘못된 앱 사이트 연결 ID 형식입니다.", + "app-store-link": "App Store 링크", + "app-store-link-required": "App Store 링크가 필요합니다", + "application-details": "애플리케이션 상세", + "application-package": "애플리케이션 패키지", + "application-secret": "애플리케이션 시크릿", + "application-secret-required": "애플리케이션 시크릿이 필요합니다", + "application": "애플리케이션", + "applications": "애플리케이션", + "copy-app-id": "앱 ID 복사", + "copy-app-store-link": "App Store 링크 복사", + "copy-application-package": "애플리케이션 패키지 복사", + "copy-application-secret": "애플리케이션 비밀키 복사", + "copy-google-play-link": "Google Play 링크 복사", + "copy-sha256-certificate-fingerprints": "SHA256 인증서 지문 복사", + "delete-application": "애플리케이션 삭제", + "delete-application-button-text": "결과를 이해하며 애플리케이션을 삭제합니다", + "delete-application-text": "이 작업은 취소할 수 없습니다. 애플리케이션이 영구적으로 삭제됩니다.
영구 삭제하지 않으려면 애플리케이션을 임시로 중단할 수 있습니다.
그래도 애플리케이션을 삭제하려면 \"{{phrase}}\"를 입력하여 확인하세요.", + "delete-application-title-short": "애플리케이션 '{{name}}'을(를) 삭제하시겠습니까?", + "delete-application-text-short": "주의: 확인 후 애플리케이션과 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-application-phrase": "애플리케이션 삭제", + "delete-applications-bundle-text": "주의: 확인 후 모바일 번들과 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-applications-bundle-title": "모바일 번들 '{{bundleName}}'을(를) 삭제하시겠습니까?", + "generate-application-secret": "애플리케이션 비밀키 생성", + "google-play-link": "Google Play 링크", + "google-play-link-required": "Google Play 링크는 필수 항목입니다.", + "latest-version": "최신 버전", + "min-version": "최소 버전", + "invalid-version-pattern": "잘못된 버전 형식입니다. major.minor.patch 형식을 사용하세요 (예: 1.0.0).", + "mobile-center": "모바일 센터", + "mobile-package": "애플리케이션 패키지", + "mobile-package-max-length": "애플리케이션 패키지는 256자 미만이어야 합니다", + "mobile-package-required": "애플리케이션 패키지가 필요합니다.", + "mobile-package-pattern": "애플리케이션 패키지 형식이 올바르지 않습니다", + "mobile-package-title": "애플리케이션 제목", + "mobile-package-title-max-length": "애플리케이션 제목은 256자 미만이어야 합니다", + "no-application": "애플리케이션을 찾을 수 없습니다", + "no-bundles": "번들을 찾을 수 없습니다", + "platform-type": "플랫폼 유형", + "search-application": "애플리케이션 검색", + "search-bundles": "번들 검색", + "set": "설정", + "sha256-certificate-fingerprints": "SHA256 인증서 지문", + "sha256-certificate-fingerprints-required": "SHA256 인증서 지문은 필수 항목입니다.", + "sha256-certificate-fingerprints-pattern": "잘못된 SHA256 인증서 지문 형식입니다.", + "show-hidden-pages": "숨겨진 페이지 표시", + "status": "상태", + "status-type": { + "deprecated": "사용 중단됨", + "draft": "초안", + "published": "게시됨", + "suspended": "일시 중지" + }, + "store-information": "스토어 정보", + "version-information": "버전 정보", + "min-version-release-notes": "최소 버전 릴리스 정보", + "latest-version-release-notes": "최신 버전 릴리스 정보", + "bundle": "번들", + "bundles": "번들", + "add-bundle": "번들 추가", + "title": "제목", + "title-required": "제목은 필수 항목입니다", + "title-cannot-contain-only-spaces": "제목은 공백만 포함할 수 없습니다.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "oauth-clients": "OAuth 2.0 클라이언트", + "android-app": "Android 앱", + "android-application": "Android 애플리케이션", + "ios-app": "iOS 앱", + "ios-application": "iOS 애플리케이션", + "invalid-store-link": "잘못된 스토어 링크입니다.", + "enable-oauth": "OAuth 2.0 활성화", + "enable-self-registration": "셀프 등록 활성화", + "edit-bundle": "번들 편집", + "description": "설명", + "basic-settings": "기본 설정", + "no-application-matching": "'{{entity}}'와(과) 일치하는 애플리케이션을 찾을 수 없습니다.", + "no-bundle-matching": "'{{entity}}'와(과) 일치하는 번들을 찾을 수 없습니다.", + "application-required": "애플리케이션이 필요합니다.", + "bundle-required": "번들이 필요합니다.", + "no-application-text": "애플리케이션을 찾을 수 없습니다", + "no-bundle-text": "번들을 찾을 수 없습니다", + "layout": "레이아웃", + "pages": "페이지", + "hide-all-pages": "모든 페이지 숨기기", + "reset-to-default-pages": "기본 페이지로 재설정", + "add-specific-page": "특정 페이지 추가", + "visible": "표시", + "hidden": "숨김", + "reset-to-page-default": "페이지를 기본값으로 재설정", + "mobile-599": "모바일 (최대 599px)", + "tablet-959": "태블릿 (최대 959px)", + "max-element-number": "최대 요소 수", + "page-name": "페이지 이름", + "page-name-required": "페이지 이름은 필수 항목입니다.", + "page-name-cannot-contain-only-spaces": "페이지 이름은 공백만 포함할 수 없습니다.", + "page-name-max-length": "페이지 이름은 256자 미만이어야 합니다.", + "page-type": "페이지 유형", + "pages-types": { + "dashboard": "대시보드", + "web-view": "웹 보기", + "custom": "사용자 지정" + }, + "url": "URL", + "invalid-url-format": "잘못된 URL 형식입니다.", + "path": "경로", + "invalid-path-format": "잘못된 경로 형식입니다.", + "custom-page": "사용자 지정 페이지", + "edit-page": "페이지 편집", + "edit-custom-page": "사용자 지정 페이지 편집", + "delete-page": "페이지 삭제", + "qr-code-widget": "QR 코드 위젯", + "type-here": "유형 히어로", + "configuration-dialog": "구성 대화상자", + "configuration-app": "구성 앱", + "configuration-step": { + "prepare-environment-title": "개발 환경 준비", + "prepare-environment-text": "Flutter ThingsBoard 모바일 애플리케이션에는 Flutter SDK가 필요합니다. 지침에 따라 Flutter SDK를 설정하세요.", + "get-source-code-title": "앱 소스 코드 얻기", + "get-source-code-text": "GitHub 저장소에서 복제하여 Flutter ThingsBoard 모바일 애플리케이션 소스 코드를 얻을 수 있습니다:", + "configure-app-settings-title": "앱 설정 구성", + "configure-app-settings-text": "구성 파일을 다운로드하여 이전 단계에서 복제한 프로젝트의 루트 디렉토리에 배치하세요.", + "download-file": "파일 다운로드", + "run-app-title": "앱 실행", + "run-app-text": "IDE에 설명된 대로 앱을 실행하세요.\n터미널을 사용하는 경우 다음 명령으로 앱을 실행하세요:", + "more-information": "자세한 정보는 시작하기 문서에서 찾을 수 있습니다.", + "getting-started": "시작하기" + } + }, + "notification": { + "action-button": "액션 버튼", + "action-type": "액션 유형", + "active": "활성", + "add-notification-recipients-group": "알림 수신자 그룹 추가", + "add-notification-template": "알림 템플릿 추가", + "add-recipient": "수신자 추가", + "add-recipients": "수신자 추가", + "add-rule": "규칙 추가", + "add-stage": "단계 추가", + "add-template": "템플릿 추가", + "after": "이후", + "alarm-assignment-trigger-settings": "알람 할당 트리거 설정", + "alarm-comment-trigger-settings": "알람 댓글 트리거 설정", + "alarm-trigger-settings": "알람 트리거 설정", + "all": "모두", + "api-feature-hint": "필드가 비어 있으면 트리거가 모든 API 기능에 적용됩니다.", + "api-usage-trigger-settings": "API 사용 트리거 설정", + "new-platform-version-trigger-settings": "새 플랫폼 버전 트리거 설정", + "rate-limits-trigger-settings": "속도 제한 초과 트리거 설정", + "task-processing-failure-trigger-settings": "작업 처리 실패 트리거 설정", + "resources-shortage-trigger-settings": "리소스 부족 트리거 설정", + "at-least-one-should-be-selected": "하나 이상 선택해야 합니다", + "basic-settings": "기본 설정", + "button-text": "버튼 텍스트", + "button-text-required": "버튼 텍스트가 필요합니다", + "button-text-max-length": "버튼 텍스트는 {{ length }}자 이하여야 합니다", + "compose": "작성", + "conversation": "대화", + "conversation-required": "대화는 필수 항목입니다.", + "copy-notification-template": "알림 템플릿 복사", + "copy-rule": "규칙 복사", + "copy-template": "템플릿 복사", + "create-new": "새로 만들기", + "created": "생성됨", + "customize-messages": "메시지 사용자 지정", + "cpu-threshold": "CPU 임계값", + "delete-notification-text": "주의: 확인 후 알림을 복구할 수 없게 됩니다.", + "delete-notification-title": "알림을 삭제하시겠습니까?", + "delete-notifications-text": "주의: 확인 후 알림을 복구할 수 없게 됩니다.", + "delete-notifications-title": "{ count, plural, =1 {1개 알림} other {#개 알림} }을(를) 삭제하시겠습니까?", + "delete-recipient-text": "주의: 확인 후 수신자를 복구할 수 없게 됩니다.", + "delete-recipient-title": "수신자 '{{recipientName}}'을(를) 삭제하시겠습니까?", + "delete-recipients-text": "주의: 확인 후 수신자를 복구할 수 없게 됩니다.", + "delete-recipients-title": "{ count, plural, =1 {1개 수신자} other {#개 수신자} }를 삭제하시겠습니까?", + "delete-request-text": "주의: 확인 후 요청을 복구할 수 없게 됩니다.", + "delete-request-title": "요청을 삭제하시겠습니까?", + "delete-requests-text": "주의: 확인 후 요청을 복구할 수 없게 됩니다.", + "delete-requests-title": "{ count, plural, =1 {1개 요청} other {#개 요청} }을(를) 삭제하시겠습니까?", + "delete-rule-text": "주의: 확인 후 규칙을 복구할 수 없게 됩니다.", + "delete-rule-title": "규칙 '{{ruleName}}'을(를) 삭제하시겠습니까?", + "delete-rules-text": "주의: 확인 후 규칙을 복구할 수 없게 됩니다.", + "delete-rules-title": "{ count, plural, =1 {1개 규칙} other {#개 규칙} }을(를) 삭제하시겠습니까?", + "delete-template-text": "주의: 확인 후 템플릿을 복구할 수 없게 됩니다.", + "delete-template-title": "템플릿 '{{templateName}}'을(를) 삭제하시겠습니까?", + "delete-templates-text": "주의: 확인 후 템플릿을 복구할 수 없게 됩니다.", + "delete-templates-title": "{ count, plural, =1 {1개 템플릿} other {#개 템플릿} }을(를) 삭제하시겠습니까?", + "deleted": "삭제됨", + "delivery-method": { + "delivery-method": "전달 방법", + "email": "이메일", + "email-preview": "이메일 알림 미리보기", + "slack": "Slack", + "slack-preview": "Slack 알림 미리보기", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams 알림 미리보기", + "sms": "SMS", + "sms-preview": "SMS 알림 미리보기", + "web": "웹", + "web-preview": "웹 알림 미리보기", + "mobile-app": "모바일 앱", + "mobile-app-preview": "모바일 앱 알림 미리보기" + }, + "delivery-method-not-configure-click": "전달 방법이 구성되지 않았습니다. 클릭하여 설정하세요.", + "delivery-method-not-configure-contact": "전달 방법이 구성되지 않았습니다. 시스템 관리자에게 문의하세요.", + "delivery-methods": "전달 방법", + "description": "설명", + "device-activity-trigger-settings": "장치 활성 트리거 설정", + "device-list-rule-hint": "필드가 비어 있으면 트리거가 모든 장치에 적용됩니다.", + "device-profiles-list-rule-hint": "필드가 비어 있으면 트리거가 모든 장치 프로필에 적용됩니다", + "disabled": "비활성화됨", + "edge-trigger-settings": "Edge 트리거 설정", + "edge-list-rule-hint": "필드가 비어 있으면 트리거가 모든 엣지 인스턴스에 적용됩니다", + "edit-notification-recipients-group": "알림 수신자 그룹 편집", + "edit-notification-template": "알림 템플릿 편집", + "edit-rule": "규칙 편집", + "edit-template": "템플릿 편집", + "enabled": "사용", + "entities-limit-trigger-settings": "엔티티 제한 트리거 설정", + "entity-action-trigger-settings": "엔티티 동작 트리거 설정", + "entity-type": "엔티티 유형", + "escalation-chain": "에스컬레이션 체인", + "failed-send": "전송 실패", + "fails": "{ count, plural, =1 {1회 실패} other {#회 실패} }", + "filter": "필터", + "first-recipient": "첫 번째 수신자", + "inactive": "비활성", + "inbox": "받은편지함", + "notification-inbox": "알림 / 받은편지함", + "input-field-support-templatization": "입력 필드는 템플릿화를 지원합니다.", + "input-fields-support-templatization": "입력 필드는 템플릿화를 지원합니다.", + "link": "링크", + "link-required": "링크는 필수 항목입니다.", + "link-max-length": "링크는 {{ length }}자 이하여야 합니다.", + "link-type": { + "dashboard": "대시보드 열기", + "link": "URL 링크 열기" + }, + "loading-notifications": "알림 로딩 중...", + "management": "알림 관리", + "mark-all-as-read": "모두 읽음으로 표시", + "mark-as-read": "읽음으로 표시", + "message": "메시지", + "message-required": "메시지는 필수 항목입니다.", + "message-max-length": "메시지는 {{ length }}자 이하여야 합니다.", + "name": "이름", + "name-required": "이름은 필수 항목입니다.", + "new-notification": "새 알림", + "no-inbox-notification": "알림을 찾을 수 없음", + "no-notification-request": "알림 요청 없음", + "no-notification-templates": "알림 템플릿을 찾을 수 없음", + "no-notifications-yet": "아직 알림 없음", + "no-recipients-notification": "수신자 알림 없음", + "no-recipients-matching": "'{{entity}}'와(과) 일치하는 수신자를 찾을 수 없습니다.", + "no-recipients-text": "수신자를 찾을 수 없음", + "no-rule": "구성된 규칙 없음", + "no-rules-notification": "규칙 알림 없음", + "no-severity-found": "심각도를 찾을 수 없음", + "no-severity-matching": "'{{severity}}'을(를) 찾을 수 없습니다.", + "no-template-matching": "'{{template}}'와(과) 일치하는 리소스를 찾을 수 없습니다.", + "create-new-template": "새로 만들기!", + "not-found-slack-recipient": "Slack 수신자를 찾을 수 없음", + "notification": "알림", + "notification-center": "알림 센터", + "notification-tap-action": "알림 탭 동작", + "notification-tap-action-hint": "비활성화하면 기본 알람 대시보드가 사용됩니다.", + "notify": "알림", + "notify-again": "다시 알림", + "notify-alarm-action": { + "acknowledged": "확인", + "assigned": "알람 할당됨", + "cleared": "해제", + "created": "알람 생성됨", + "severity-changed": "알람 심각도 변경됨", + "unassigned": "알람 할당 해제" + }, + "notify-on": "알림 설정", + "notify-on-comment-update": "댓글 업데이트 시 알림", + "notify-on-required": "알림 설정은 필수 항목입니다.", + "notify-on-unassign": "할당 해제 시 알림", + "notify-only-user-comments": "사용자 댓글에만 알림", + "only-rule-chain-lifecycle-failures": "규칙 체인 수명 주기 실패만", + "only-rule-node-lifecycle-failures": "규칙 노드 수명 주기 실패만", + "platform-users": "플랫폼 사용자", + "ram-threshold": "RAM 임계값", + "rate-limits": "속도 제한", + "rate-limits-hint": "필드가 비어 있으면 트리거가 모든 속도 제한에 적용됩니다.", + "recipient": "수신자", + "recipient-group": "수신자 그룹", + "recipient-type": { + "affected-tenant-administrators": "영향받는 테넌트 관리자", + "affected-user": "영향받는 사용자", + "all-users": "모든 사용자", + "customer-users": "고객 사용자", + "system-administrators": "시스템 관리자", + "tenant-administrators": "테넌트 관리자", + "user-filters": "사용자 필터", + "user-list": "사용자 목록", + "users-entity-owner": "엔티티 소유자의 사용자" + }, + "recipients": "수신자", + "notification-recipient": "알림 수신자", + "notification-recipient-required": "알림 수신자는 필수 항목입니다.", + "notification-recipients": "알림 / 수신자", + "recipients-count": "{ count, plural, =1 {1명 수신자} other {#명 수신자} }", + "recipients-required": "수신자는 필수 항목입니다.", + "refresh-allow-delivery-method": "전달 방법 새로고침 허용", + "request-search": "요청 검색", + "request-status": { + "processing": "처리 중", + "scheduled": "예약됨", + "sent": "전송됨" + }, + "review": "검토", + "rule": "규칙", + "rule-chain-list-rule-hint": "필드가 비어 있으면 트리거가 모든 규칙 체인에 적용됩니다.", + "rule-engine-events-trigger-settings": "규칙 엔진 이벤트 트리거 설정", + "rule-engine-filter": "규칙 엔진 필터", + "rule-name": "규칙 이름", + "rule-name-required": "이름은 필수 항목입니다.", + "rule-disable": "알림 규칙 비활성화", + "rule-enable": "알림 규칙 활성화", + "rule-node-filter": "규칙 노드 필터", + "rules": "규칙", + "notification-rules": "알림 / 규칙", + "scheduler-later": "나중에 예약", + "search-notification": "알림 검색", + "search-recipients": "수신자 검색", + "search-rules": "규칙 검색", + "search-templates": "템플릿 검색", + "see-documentation": "문서 보기", + "selected-notifications": "{ count, plural, =1 {1개 알림} other {#개 알림} } 선택됨", + "selected-recipients": "{ count, plural, =1 {1명 수신자} other {#명 수신자} } 선택됨", + "selected-requests": "{ count, plural, =1 {1개 요청} other {#개 요청} } 선택됨", + "selected-rules": "{ count, plural, =1 {1개 규칙} other {#개 규칙} } 선택됨", + "selected-template": "{ count, plural, =1 {1개 템플릿} other {#개 템플릿} } 선택됨", + "send-notification": "알림 전송", + "sent": "전송됨", + "setup": "설정", + "notification-sent": "알림 / 전송됨", + "set-entity-from-notification": "알림에서 대시보드 상태로 엔티티 설정", + "slack-chanel-type": "Slack 채널 유형", + "slack-chanel-types": { + "direct": "다이렉트 메시지", + "private-channel": "비공개 채널", + "public-channel": "공개 채널" + }, + "start-from-scratch": "처음부터 시작", + "status": "상태", + "stop-escalation-alarm-status-become": "알람 상태가 다음이 되면 에스컬레이션 중지:", + "storage-threshold": "저장소 임계값", + "subject": "제목", + "subject-required": "제목이 필요합니다", + "subject-max-length": "제목은 {{ length }}자 이하여야 합니다", + "template": "템플릿", + "template-name": "템플릿 이름", + "template-required": "템플릿이 필요합니다", + "template-type": { + "alarm": "알람", + "alarm-assignment": "알람 할당", + "alarm-comment": "알람 댓글", + "api-usage-limit": "API 사용 한도", + "device-activity": "장치 활동", + "entities-limit": "엔티티 한도", + "entities-limit-increase-request": "엔티티 한도 증가 요청", + "entity-action": "엔티티 작업", + "general": "일반", + "rule-engine-lifecycle-event": "규칙 엔진 수명 주기 이벤트", + "rule-node": "규칙 노드", + "new-platform-version": "새 플랫폼 버전", + "rate-limits": "속도 제한 초과", + "edge-communication-failure": "엣지 통신 실패", + "edge-connection": "엣지 연결", + "task-processing-failure": "작업 처리 실패", + "resources-shortage": "리소스 부족" + }, + "templates": "템플릿", + "notification-templates": "알림 / 템플릿", + "tenant-profiles-list-rule-hint": "필드가 비어 있으면 트리거가 모든 테넌트 프로필에 적용됩니다", + "tenants-list-rule-hint": "필드가 비어 있으면 트리거가 모든 테넌트에 적용됩니다", + "threshold": "임계값", + "theme-color": "테마 색상", + "time": "시간", + "track-rule-node-events": "규칙 노드 이벤트 추적", + "trigger": { + "alarm": "알람", + "alarm-assignment": "알람 할당", + "alarm-comment": "알람 댓글", + "api-usage-limit": "API 사용 한도", + "device-activity": "장치 활동", + "entities-limit": "엔티티 한도", + "entity-action": "엔티티 작업", + "rule-engine-lifecycle-event": "규칙 엔진 수명 주기 이벤트", + "new-platform-version": "새 플랫폼 버전", + "rate-limits": "속도 제한 초과", + "edge-connection": "엣지 연결", + "edge-communication-failure": "엣지 통신 실패", + "task-processing-failure": "작업 처리 실패", + "resources-shortage": "리소스 부족", + "trigger": "트리거", + "trigger-required": "트리거가 필요합니다" + }, + "type": "유형", + "unread": "읽지 않음", + "updated": "수정됨", + "use-deprecated-webhook-connectors": "더 이상 사용되지 않는 웹훅 커넥터 사용", + "use-old-api": "이전 API 사용", + "use-template": "템플릿 사용", + "view-all": "모두 보기", + "warning": "경고", + "webhook-url": "웹훅 URL", + "webhook-url-required": "웹훅 URL이 필요합니다", + "workflow-url": "워크플로 URL", + "workflow-url-required": "워크플로 URL이 필요합니다", + "channel-name": "채널 이름", + "channel-name-required": "채널 이름이 필요합니다", + "settings": { + "notification-settings": "알림 설정", + "reset-all": "모든 설정 재설정", + "reset-all-title": "폼을 재설정하시겠습니까?", + "reset-all-text": "확인 후 설정 폼이 기본값으로 재설정되고 저장됩니다.", + "type": "유형", + "enable-all": "모두 활성화", + "disable-all": "모두 비활성화", + "delivery-not-configured": "배달 방법이 구성되지 않음" + } + }, + "ota-update": { + "add": "패키지 추가", + "assign-firmware": "할당된 펌웨어", + "assign-firmware-required": "할당된 펌웨어가 필요합니다", + "assign-software": "할당된 소프트웨어", + "assign-software-required": "할당된 소프트웨어가 필요합니다", + "auto-generate-checksum": "체크섬 자동 생성", + "checksum": "체크섬", + "checksum-hint": "체크섬이 비어 있으면 자동으로 생성됩니다.", + "checksum-algorithm": "체크섬 알고리즘", + "checksum-copied-message": "패키지 체크섬이 클립보드에 복사되었습니다.", + "change-firmware": "펌웨어 변경으로 { count, plural, =1 {1개 장치} other {#개 장치} }가 업데이트될 수 있습니다.", + "change-software": "소프트웨어 변경으로 { count, plural, =1 {1개 장치} other {#개 장치} }가 업데이트될 수 있습니다.", + "change-ota-setting-title": "OTA 설정을 변경하시겠습니까?", + "chose-compatible-device-profile": "업로드된 패키지는 선택한 프로필이 있는 장치에서만 사용할 수 있습니다.", + "chose-firmware-distributed-device": "장치에 배포할 펌웨어를 선택하세요.", + "chose-software-distributed-device": "장치에 배포할 소프트웨어를 선택하세요.", + "content-type": "콘텐츠 유형", + "copy-checksum": "체크섬 복사", + "copy-direct-url": "직접 URL 복사", + "copyId": "패키지 ID 복사", + "copied": "복사됨!", + "delete": "패키지 삭제", + "delete-ota-update-text": "주의: 확인 후 OTA 업데이트를 복구할 수 없게 됩니다.", + "delete-ota-update-title": "OTA 업데이트 '{{title}}'을(를) 삭제하시겠습니까?", + "delete-ota-updates-text": "주의: 확인 후 선택한 모든 OTA 업데이트가 제거됩니다.", + "delete-ota-updates-title": "{ count, plural, =1 {1개 OTA 업데이트} other {#개 OTA 업데이트} }을(를) 삭제하시겠습니까?", + "description": "설명", + "direct-url": "직접 URL", + "direct-url-copied-message": "패키지 직접 URL이 클립보드에 복사되었습니다.", + "direct-url-required": "직접 URL은 필수 항목입니다.", + "download": "패키지 다운로드", + "drop-file": "패키지 파일을 끌어다 놓거나 클릭하여 업로드할 파일을 선택하세요.", + "drop-package-file-or": "패키지 파일을 끌어다 놓거나", + "file-name": "파일 이름", + "file-size": "파일 크기", + "file-size-bytes": "파일 크기 (바이트)", + "idCopiedMessage": "패키지 ID가 클립보드에 복사되었습니다.", + "no-firmware-matching": "'{{entity}}'과(와) 일치하는 호환되는 펌웨어 OTA 업데이트 패키지가 없습니다.", + "no-firmware-text": "프로비저닝된 호환되는 펌웨어 OTA 업데이트 패키지가 없습니다.", + "no-packages-text": "패키지를 찾을 수 없습니다", + "no-software-matching": "'{{entity}}'과(와) 일치하는 호환되는 소프트웨어 OTA 업데이트 패키지가 없습니다.", + "no-software-text": "프로비저닝된 호환되는 소프트웨어 OTA 업데이트 패키지가 없습니다.", + "ota-update": "OTA 업데이트", + "ota-update-details": "OTA 업데이트 세부 정보", + "ota-updates": "OTA 업데이트", + "package-file": "패키지 파일", + "package-type": "패키지 유형", + "packages-repository": "패키지 저장소", + "search": "패키지 검색", + "selected-package": "{ count, plural, =1 {1개 패키지} other {#개 패키지} } 선택됨", + "title": "제목", + "title-required": "제목을 입력하세요.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "types": { + "firmware": "펌웨어", + "software": "소프트웨어" + }, + "upload-binary-file": "바이너리 파일 업로드", + "use-external-url": "외부 URL 사용", + "version": "버전", + "version-required": "버전은 필수 항목입니다.", + "version-tag": "버전 태그", + "version-tag-hint": "사용자 지정 태그는 장치에서 보고한 패키지 버전과 일치해야 합니다.", + "version-max-length": "버전은 256자 미만이어야 합니다.", + "warning-after-save-no-edit": "패키지가 업로드되면 제목, 버전, 장치 프로필 및 패키지 유형을 수정할 수 없습니다." }, "position": { "top": "상단", @@ -1728,14 +4870,109 @@ "right": "오른쪽" }, "profile": { - "profile": "프로파일", + "profile": "프로필", "last-login-time": "마지막 로그인", "change-password": "비밀번호 변경", - "current-password": "현재 비밀번호" + "current-password": "현재 비밀번호", + "copy-jwt-token": "JWT 토큰 복사", + "jwt-token": "JWT 토큰", + "token-valid-till": "토큰 유효 기간", + "tokenCopiedSuccessMessage": "JWT 토큰이 클립보드에 복사되었습니다.", + "tokenCopiedWarnMessage": "JWT 토큰이 만료되었습니다! 페이지를 새로고침하세요." + }, + "profiles": { + "profiles": "프로필" + }, + "security": { + "security": "보안", + "general-settings": "일반 보안 설정", + "access-token": "액세스 토큰", + "access-token-required": "액세스 토큰이 필요합니다", + "clientId": "클라이언트 ID", + "clientId-required": "클라이언트 ID가 필요합니다", + "username": "사용자명", + "username-required": "사용자 이름은 필수 항목입니다.", + "ca-cert": "CA 인증서", + "2fa": { + "2fa": "2단계 인증", + "2fa-description": "2단계 인증은 무단 액세스로부터 계정을 보호합니다. 로그인할 때 보안 코드를 입력하기만 하면 됩니다.", + "authenticate-with": "다음으로 인증할 수 있습니다:", + "disable-2fa-provider-text": "{{name}}을(를) 비활성화하면 계정 보안이 약화됩니다.", + "disable-2fa-provider-title": "{{name}}을(를) 비활성화하시겠습니까?", + "get-new-code": "새 코드 받기", + "main-2fa-method": "기본 2단계 인증 방법으로 사용", + "dialog": { + "activation-step-description-email": "다음에 로그인할 때 이메일 주소로 전송될 보안 코드를 입력하라는 메시지가 표시됩니다.", + "activation-step-description-sms": "다음에 로그인할 때 전화번호로 전송될 보안 코드를 입력하라는 메시지가 표시됩니다.", + "activation-step-description-totp": "다음에 로그인할 때 2단계 인증 코드를 제공해야 합니다.", + "activation-step-label": "활성화", + "backup-code-description": "코드를 인쇄하여 계정에 로그인할 때 필요할 때 사용할 수 있도록 하세요. 각 백업 코드는 한 번만 사용할 수 있습니다.", + "backup-code-warn": "이 페이지를 떠나면 이 코드를 다시 볼 수 없습니다. 아래 옵션을 사용하여 안전하게 보관하세요.", + "download-txt": "다운로드 (txt)", + "email-step-description": "인증기로 사용할 이메일을 입력하세요.", + "email-step-label": "이메일", + "enable-email-title": "이메일 인증기 활성화", + "enable-sms-title": "SMS 인증기 활성화", + "enable-totp-title": "인증기 앱 활성화", + "enter-verification-code": "여기에 6자리 코드를 입력하세요", + "get-backup-code-title": "백업 코드 받기", + "next": "다음", + "scan-qr-code": "인증 앱으로 이 QR 코드를 스캔하세요", + "send-code": "코드 전송", + "sms-step-description": "인증기로 사용할 전화번호를 입력하세요.", + "sms-step-label": "전화번호", + "success": "성공!", + "totp-step-description-install": "Google Authenticator, Authy 또는 Duo와 같은 앱을 설치할 수 있습니다.", + "totp-step-description-open": "휴대전화에서 인증기 앱을 엽니다.", + "totp-step-label": "앱 받기", + "verification-code": "6자리 코드", + "verification-code-invalid": "잘못된 확인 코드 형식입니다.", + "verification-code-incorrect": "확인 코드가 올바르지 않습니다.", + "verification-code-many-request": "확인 코드 확인 요청이 너무 많습니다.", + "verification-step-description": "{{address}}(으)로 방금 전송된 6자리 코드를 입력하세요", + "verification-step-label": "확인" + }, + "provider": { + "email": "이메일", + "email-description": "이메일 주소로 전송된 보안 코드를 사용하여 인증하세요.", + "email-hint": "인증 코드가 {{ info }}(으)로 이메일로 전송됩니다", + "sms": "SMS", + "sms-description": "휴대전화를 사용하여 인증하세요. 로그인할 때 SMS 메시지로 보안 코드를 보내드립니다.", + "sms-hint": "인증 코드가 {{ info }}(으)로 문자 메시지로 전송됩니다", + "totp": "인증기 앱", + "totp-description": "휴대전화에서 Google Authenticator, Authy 또는 Duo와 같은 앱을 사용하여 인증하세요. 로그인할 보안 코드를 생성합니다.", + "totp-hint": "계정에 인증기 앱이 설정되었습니다", + "backup_code": "백업 코드", + "backup-code-description": "이 인쇄 가능한 일회용 암호는 여행 중처럼 휴대전화를 사용할 수 없을 때 로그인할 수 있도록 해줍니다.", + "backup-code-hint": "현재 {{ info }}개의 일회용 코드가 활성화되어 있습니다." + } + }, + "password-requirement": { + "at-least": "최소:", + "character": "{ count, plural, =1 {1자} other {#자} }", + "digit": "{ count, plural, =1 {1자리 숫자} other {#자리 숫자} }", + "password-tooltip-min-length": "최소 {{minimumLength}}자 이상", + "password-tooltip-max-length": "최대 {{maximumLength}}자 이하", + "password-tooltip-uppercase": "{{minimumUppercaseLetters}}개 대문자", + "password-tooltip-lowercase": "{{minimumLowercaseLetters}}개 소문자", + "password-tooltip-digit": "{{minimumDigits}}개 숫자", + "password-tooltip-special-characters": "{{minimumSpecialCharacters}}개 특수 문자", + "incorrect-password-try-again": "잘못된 비밀번호입니다. 다시 시도하세요.", + "lowercase-letter": "{ count, plural, =1 {1개 소문자} other {#개 소문자} }", + "new-passwords-not-match": "새 비밀번호가 일치하지 않습니다.", + "password-should-not-contain-spaces": "비밀번호에 공백이 포함되지 않아야 합니다.", + "password-not-meet-requirements": "비밀번호가 요구 사항을 충족하지 않습니다.", + "password-requirements": "비밀번호 요구 사항", + "password-should-difference": "새 비밀번호는 현재 비밀번호와 달라야 합니다.", + "special-character": "{ count, plural, =1 {1개 특수 문자} other {#개 특수 문자} }", + "uppercase-letter": "{ count, plural, =1 {1개 대문자} other {#개 대문자} }", + "at-most": "최대:" + } }, "relation": { "relations": "관계", "direction": "방향", + "clear-relation-type": "관계 유형 지우기", "search-direction": { "FROM": "보내는 사람", "TO": "받는 사람" @@ -1748,41 +4985,135 @@ "to-relations": "수신 관계", "selected-relations": "{ count, plural, =1 {1 개 관계} other {# 개 관계} } 선택됨", "type": "유형", - "to-entity-type": "받는 개체 유형", - "to-entity-name": "받는 개체 이름", - "from-entity-type": "보내는 개체 유형", - "from-entity-name": "받는 개체 이름", - "to-entity": "받는 개체", - "from-entity": "보내는 개체", + "to-entity-type": "받는 엔티티 유형", + "to-entity-name": "받는 엔티티 이름", + "from-entity-type": "보내는 엔티티 유형", + "from-entity-name": "받는 엔티티 이름", + "to-entity": "받는 엔티티", + "from-entity": "보내는 엔티티", "delete": "관계 삭제", "relation-type": "관계 유형", "relation-type-required": "관계 유형을 입력하세요.", + "relation-type-max-length": "관계 유형은 256자 미만이어야 합니다.", "any-relation-type": "전체 유형", "add": "관계 추가", "edit": "관계 편집", - "delete-to-relation-title": "개체 '{{entityName}}'을(를) 삭제하시겠습니까?", - "delete-to-relation-text": "주의: 개체 '{{entityName}}'는 현재 개체와 관계가 해제됩니다.", + "delete-to-relation-title": "엔티티 '{{entityName}}'을(를) 삭제하시겠습니까?", + "delete-to-relation-text": "주의: 엔티티 '{{entityName}}'는 현재 엔티티와 관계가 해제됩니다.", "delete-to-relations-title": "{ count, plural, =1 {1 개 관계} other {# 개 관계} }를 삭제하시겠습니까?", - "delete-to-relations-text": "주의: 모든 선택된 관계는 제거되며, 해당되는 개체는 현재의 개체와 관계가 해제됩니다.", - "delete-from-relation-title": "개체 '{{entityName}}'의 관계를 삭제하시겠습니까?", - "delete-from-relation-text": "주의: 현재 개체는 개체 '{{entityName}}'와의 관계가 해제됩니다.", + "delete-to-relations-text": "주의: 모든 선택된 관계는 제거되며, 해당되는 엔티티는 현재의 엔티티와 관계가 해제됩니다.", + "delete-from-relation-title": "엔티티 '{{entityName}}'의 관계를 삭제하시겠습니까?", + "delete-from-relation-text": "주의: 현재 엔티티는 엔티티 '{{entityName}}'와의 관계가 해제됩니다.", "delete-from-relations-title": "{ count, plural, =1 {1 개 관계} other {# 개 관계} }를 삭제하시겠습니까?", - "delete-from-relations-text": "주의: 모든 선택된 관계는 제거되며, 현제 개체는 해당되는 대체와 관계가 해제됩니다.", + "delete-from-relations-text": "주의: 모든 선택된 관계는 제거되며, 현제 엔티티는 해당되는 대체와 관계가 해제됩니다.", "remove-relation-filter": "관계 필터 삭제", + "remove-filter": "필터 제거", "add-relation-filter": "관계 필터 추가", "any-relation": "전체 관계", "relation-filters": "관계 필터", + "relation-filter": "관계 필터", "additional-info": "추가 정보 (JSON)", "invalid-additional-info": "추가적인 정보 JSON을 파싱할 수 없습니다.", - "no-relations-text": "아무 관계도 찾을 수 없습니다" + "no-relations-text": "아무 관계도 찾을 수 없습니다", + "not": "제외", + "copy-type": "유형 복사" + }, + "resource": { + "add": "리소스 추가", + "all-types": "모두", + "copyId": "리소스 ID 복사", + "delete": "리소스 삭제", + "delete-resource-text": "주의: 확인 후 리소스를 복구할 수 없게 됩니다.", + "delete-resource-title": "리소스 '{{resourceTitle}}'을(를) 삭제하시겠습니까?", + "delete-resources-action-title": "{ count, plural, =1 {1개 리소스} other {#개 리소스} } 삭제", + "delete-resources-text": "선택한 리소스는 장치 프로필에서 사용 중이더라도 삭제됩니다.", + "delete-resources-title": "{ count, plural, =1 {1개 리소스} other {#개 리소스} }를 삭제하시겠습니까?", + "download": "리소스 다운로드", + "drop-file": "리소스 파일을 끌어다 놓거나 클릭하여 업로드할 파일을 선택하세요.", + "drop-resource-file-or": "리소스 파일을 끌어다 놓거나", + "empty": "리소스가 비어 있습니다", + "file-name": "파일 이름", + "idCopiedMessage": "리소스 ID가 클립보드에 복사되었습니다.", + "no-resource-matching": "'{{widgetsBundle}}'와(과) 일치하는 리소스를 찾을 수 없습니다.", + "no-resource-text": "리소스를 찾을 수 없습니다", + "open-widgets-bundle": "위젯 번들 열기", + "resource": "리소스", + "resource-file": "리소스 파일", + "resource-files": "리소스 파일", + "resource-library-details": "리소스 세부 정보", + "resource-type": "리소스 유형", + "resources-library": "리소스 라이브러리", + "search": "리소스 검색", + "selected-resources": "{ count, plural, =1 {1개 리소스} other {#개 리소스} } 선택됨", + "system": "시스템", + "title": "제목", + "title-required": "제목을 입력하세요.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "type": { + "jks": "JKS", + "js-module": "JS 모듈", + "lwm2m-model": "LWM2M 모델", + "pkcs-12": "PKCS #12", + "general": "일반" + }, + "resource-sub-type": "하위 유형", + "sub-type": { + "image": "이미지", + "scada-symbol": "SCADA 심볼", + "extension": "확장", + "module": "모듈" + }, + "resource-is-in-use": "리소스가 다른 엔티티에서 사용 중입니다", + "resources-are-in-use": "리소스가 다른 엔티티에서 사용 중입니다", + "resource-is-in-use-text": "'{{title}}' 리소스가 다음 엔티티에서 사용 중이므로 삭제되지 않았습니다:", + "resources-are-in-use-text": "모든 리소스가 삭제되지 않았습니다. 다른 엔티티에서 사용 중이기 때문입니다.
해당 리소스 행의 참조 버튼을 클릭하여 참조된 엔티티를 볼 수 있습니다.
이 리소스를 계속 삭제하려면 아래 표에서 선택하고 선택 삭제 버튼을 클릭하세요.", + "delete-resource-in-use-text": "리소스를 계속 삭제하려면 무조건 삭제 버튼을 클릭하세요." + }, + "javascript": { + "add": "JavaScript 리소스 추가", + "delete": "JavaScript 리소스 삭제", + "delete-javascript-resource-text": "주의: 확인 후 JavaScript 리소스를 복구할 수 없게 됩니다.", + "delete-javascript-resource-title": "JavaScript 리소스 '{{resourceTitle}}'을(를) 삭제하시겠습니까?", + "delete-javascript-resources-action-title": "JavaScript { count, plural, =1 {1개 리소스} other {#개 리소스} } 삭제", + "delete-javascript-resources-text": "선택한 JavaScript 리소스는 JavaScript 함수에서 사용 중이더라도 삭제됩니다.", + "delete-javascript-resources-title": "JavaScript { count, plural, =1 {1개 리소스} other {#개 리소스} }를 삭제하시겠습니까?", + "delete-javascript-resource-in-use-text": "JavaScript 리소스를 계속 삭제하려면 무조건 삭제 버튼을 클릭하세요.", + "download": "JavaScript 리소스 다운로드", + "upload-from-file": "파일에서 JavaScript 업로드", + "resource-file": "JavaScript 리소스 파일", + "drop-file": "JavaScript 파일을 끌어다 놓거나 클릭하여 업로드할 파일을 선택하세요.", + "drop-resource-file-or": "JavaScript 파일을 끌어다 놓거나", + "javascript-library": "JavaScript 라이브러리", + "javascript-type": "JavaScript 유형", + "javascript-resource-details": "JavaScript 리소스 세부 정보", + "javascript-resource-is-in-use": "JavaScript 리소스가 다른 엔티티에서 사용 중입니다", + "javascript-resources-are-in-use": "JavaScript 리소스가 다른 엔티티에서 사용 중입니다", + "javascript-resource-is-in-use-text": "'{{title}}' JavaScript 리소스가 다음 엔티티에서 사용 중이므로 삭제되지 않았습니다:", + "javascript-resources-are-in-use-text": "모든 JavaScript 리소스가 삭제되지 않았습니다. 다른 엔티티에서 사용 중이기 때문입니다.
해당 리소스 행의 참조 버튼을 클릭하여 참조된 엔티티를 볼 수 있습니다.
이 JavaScript 리소스를 계속 삭제하려면 아래 표에서 선택하고 선택 삭제 버튼을 클릭하세요.", + "search": "JavaScript 리소스 검색", + "selected-javascript-resources": "{ count, plural, =1 {1개 JavaScript 리소스} other {#개 JavaScript 리소스} } 선택됨", + "no-javascript-resource-text": "JavaScript 리소스를 찾을 수 없습니다", + "all-types": "모두", + "module-script": "모듈 스크립트" + }, + "rpc": { + "error": { + "target-device-is-not-set": "대상 장치가 설정되지 않았습니다!", + "invalid-target-entity": "{{entityType}} 엔티티는 RPC 명령을 지원하지 않습니다.", + "failed-to-resolve-target-device": "대상 장치를 확인하지 못했습니다!", + "request-timeout": "요청 시간 초과", + "rpc-http-error": "오류: {{status}} - {{statusText}}" + } }, "rulechain": { "rulechain": "규칙 사슬", + "rulechain-events": "규칙 체인 이벤트", "rulechains": "규칙 사슬", "root": "루트", "delete": "규칙 사슬 삭제", "name": "이름", "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", "description": "설명", "add": "규칙 사슬 추가", "set-root": "규칙 사슬 루트로 전환", @@ -1814,27 +5145,53 @@ "debug-mode": "디버그 모드", "search": "규칙 사슬 검색", "selected-rulechains": "{ count, plural, =1 {1 개 규칙 사슬} other {# 개 규칙 사슬} } 선택됨", - "open-rulechain": "규칙 사슬 열기" + "open-rulechain": "규칙 사슬 열기", + "edge-template-root": "템플릿 루트", + "assign-to-edge": "엣지에 할당", + "edge-rulechain": "엣지 규칙 체인", + "unassign-rulechain-from-edge-text": "확인 후 규칙 체인이 할당 해제되어 엣지에서 액세스할 수 없습니다.", + "unassign-rulechains-from-edge-title": "{ count, plural, =1 {1개 규칙 체인} other {#개 규칙 체인} }을(를) 할당 해제하시겠습니까?", + "unassign-rulechains-from-edge-text": "확인 후 선택한 모든 규칙 체인이 할당 해제되어 엣지에서 액세스할 수 없습니다.", + "assign-rulechain-to-edge-title": "Edge에 규칙 체인 할당", + "assign-rulechain-to-edge-text": "Edge에 할당할 규칙 체인을 선택하세요", + "set-edge-template-root-rulechain": "규칙 체인을 Edge 템플릿 루트로 만들기", + "set-edge-template-root-rulechain-title": "규칙 체인 '{{ruleChainName}}'을(를) 엣지 템플릿 루트로 설정하시겠습니까?", + "set-edge-template-root-rulechain-text": "확인 후 규칙 체인이 엣지 템플릿 루트가 되며 새로 생성된 엣지의 루트 규칙 체인이 됩니다.", + "invalid-rulechain-type-error": "규칙 체인을 가져올 수 없습니다: 잘못된 규칙 체인 유형입니다. 예상 유형은 {{expectedRuleChainType}}입니다.", + "set-auto-assign-to-edge": "생성 시 Edge에 규칙 체인 할당", + "set-auto-assign-to-edge-title": "엣지 규칙 체인 '{{ruleChainName}}'을(를) 생성 시 엣지에 할당하시겠습니까?", + "set-auto-assign-to-edge-text": "확인 후 엣지 규칙 체인이 생성 시 엣지에 자동으로 할당됩니다.", + "unset-auto-assign-to-edge": "생성 시 Edge에 규칙 체인 할당 안 함", + "unset-auto-assign-to-edge-title": "엣지 규칙 체인 '{{ruleChainName}}'을(를) 생성 시 엣지에 할당하지 않으시겠습니까?", + "unset-auto-assign-to-edge-text": "확인 후 엣지 규칙 체인이 생성 시 엣지에 더 이상 자동으로 할당되지 않습니다.", + "unassign-rulechain-title": "규칙 체인 '{{ruleChainName}}'의 할당을 해제하시겠습니까?", + "unassign-rulechains": "규칙 체인 할당 해제" }, "rulenode": { + "rule-node-events": "규칙 노드 이벤트", "details": "상세 내역", "events": "이벤트", "search": "노드 검색", "open-node-library": "노드 라이브러리 열기", + "close-node-library": "노드 라이브러리 닫기", "add": "규칙 노드 추가", "name": "이름", "name-required": "이름을 입력하세요.", + "name-max-length": "이름은 256자 미만이어야 합니다.", "type": "유형", + "rule-node-description": "규칙 노드 설명", "delete": "규칙 노드 삭제", "select-all-objects": "모든 노드와 연결을 선택", "deselect-all-objects": "모든 노드와 연결을 선택 해제", "delete-selected-objects": "선택된 노드와 연결을 삭제", "delete-selected": "선택 삭제", + "create-nested-rulechain": "중첩 규칙 체인 생성", "select-all": "모두 선택", "copy-selected": "선택 복사", "deselect-all": "선택 해제", "rulenode-details": "규칙 노드 상세 정보", "debug-mode": "디버그 모드", + "singleton": "싱글톤", "configuration": "설정", "link": "링크", "link-details": "규칙 노드 링크 상세 정보", @@ -1849,45 +5206,964 @@ "no-link-label-matching": "'{{label}}' 을 찾을 수 없습니다.", "create-new-link-label": "새로 링크 라벨 만들기", "type-filter": "필터", - "type-filter-details": "Filter incoming messages with configured conditions", - "type-enrichment": "Enrichment", + "type-filter-details": "구성된 조건으로 들어오는 메시지 필터링", + "type-enrichment": "보강", "type-enrichment-details": "메시지 메타데이터에 추가적인 정보를 추가", - "type-transformation": "Transformation", - "type-transformation-details": "Change Message payload and Metadata", - "type-action": "", + "type-transformation": "변환", + "type-transformation-details": "메시지 페이로드 및 메타데이터 변경", + "type-action": "동작", "type-action-details": "특별한 조치를 수행", "type-external": "외부", - "type-external-details": "Interacts with external system", - "type-rule-chain": "Rule Chain", - "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", + "type-external-details": "외부 시스템과 상호 작용", + "type-rule-chain": "규칙 사슬", + "type-rule-chain-details": "수신 메시지를 지정된 규칙 체인으로 전달", + "type-flow": "흐름", + "type-flow-details": "메시지 흐름 구성", "type-input": "입력", - "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", - "type-unknown": "Unknown", - "type-unknown-details": "Unresolved Rule Node", - "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", - "ui-resources-load-error": "Failed to load configuration ui resources.", - "invalid-target-rulechain": "Unable to resolve target rule chain!", - "test-script-function": "Test script function", + "type-input-details": "규칙 체인의 논리적 입력, 수신 메시지를 다음 관련 규칙 노드로 전달", + "type-unknown": "알 수 없음", + "type-unknown-details": "확인되지 않은 규칙 노드", + "directive-is-not-loaded": "정의된 구성 지시문 '{{directiveName}}'을(를) 사용할 수 없습니다.", + "ui-resources-load-error": "구성 UI 리소스를 로드하지 못했습니다.", + "invalid-target-rulechain": "대상 규칙 체인을 확인할 수 없습니다!", + "test-script-function": "스크립트 함수 테스트", + "script-lang-java-script": "JavaScript", + "script-lang-tbel": "TBEL", "message": "메시지", "message-type": "메시지 유형", - "select-message-type": "메시지 유형 선택", + "select-message-type": "메시지 유형 선택", "message-type-required": "메시지 유형을 입력하세요.", "metadata": "메타데이터", "metadata-required": "메타데이터 엔트리를 입력하세요.", "output": "출력", "test": "테스트", - "help": "도움말" + "help": "도움말", + "reset-debug-settings": "모든 노드에서 디버그 설정 재설정", + "test-with-this-message": "이 메시지로 {{test}}", + "queue-hint": "메시지를 다른 큐로 전달할 큐를 선택하세요. 기본적으로 '기본' 큐가 사용됩니다.", + "queue-singleton-hint": "다중 인스턴스 환경에서 메시지 전달을 위한 큐를 선택하세요. 기본적으로 '기본' 큐가 사용됩니다." + }, + "rule-node-config": { + "id": "ID", + "additional-info": "추가 정보", + "advanced-settings": "고급 설정", + "create-entity-if-not-exists": "존재하지 않는 경우 새 엔티티 생성", + "create-entity-if-not-exists-hint": "활성화하면 지정된 매개변수가 있는 새 엔티티가 이미 존재하지 않는 한 생성됩니다. 기존 엔티티는 관계에 그대로 사용됩니다.", + "select-device-connectivity-event": "장치 연결 이벤트 선택", + "entity-name-pattern": "이름 패턴", + "device-name-pattern": "장치 이름", + "asset-name-pattern": "자산 이름", + "entity-view-name-pattern": "엔티티 뷰 이름", + "customer-title-pattern": "고객 제목", + "dashboard-name-pattern": "대시보드 제목", + "user-name-pattern": "사용자 이메일", + "edge-name-pattern": "엣지 이름", + "entity-name-pattern-required": "이름 패턴이 필요합니다", + "entity-name-pattern-hint": "이름 패턴 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "copy-message-type": "메시지 유형 복사", + "entity-type-pattern": "유형 패턴", + "entity-type-pattern-required": "유형 패턴이 필요합니다", + "message-type-value": "메시지 유형 값", + "message-type-value-required": "메시지 유형 값이 필요합니다", + "message-type-value-max-length": "메시지 유형 값은 256자 미만이어야 합니다", + "output-message-type": "출력 메시지 유형", + "entity-cache-expiration": "엔티티 캐시 만료 시간 (초)", + "entity-cache-expiration-hint": "찾은 엔티티 레코드를 저장할 수 있는 최대 시간 간격을 지정합니다. 0 값은 레코드가 만료되지 않음을 의미합니다.", + "entity-cache-expiration-required": "엔티티 캐시 만료 시간이 필요합니다.", + "entity-cache-expiration-range": "엔티티 캐시 만료 시간은 0 이상이어야 합니다.", + "customer-name-pattern": "고객 제목", + "customer-name-pattern-required": "고객 제목이 필요합니다", + "customer-name-pattern-hint": "$[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "create-customer-if-not-exists": "존재하지 않는 경우 새 고객 생성", + "unassign-from-customer": "발신자가 대시보드인 경우 특정 고객에서 할당 해제", + "unassign-from-customer-tooltip": "대시보드만 한 번에 여러 고객에게 할당할 수 있습니다.\n메시지 발신자가 대시보드인 경우 할당을 해제할 고객의 제목을 명시적으로 지정해야 합니다.", + "customer-cache-expiration": "고객 캐시 만료 시간 (초)", + "customer-cache-expiration-hint": "찾은 고객 레코드를 저장할 수 있는 최대 시간 간격을 지정합니다. 0 값은 레코드가 만료되지 않음을 의미합니다.", + "customer-cache-expiration-required": "고객 캐시 만료 시간이 필요합니다.", + "customer-cache-expiration-range": "고객 캐시 만료 시간은 0 이상이어야 합니다.", + "interval-start": "간격 시작", + "interval-end": "간격 종료", + "time-unit": "시간 단위", + "fetch-mode": "가져오기 모드", + "order-by-timestamp": "타임스탬프로 정렬", + "limit": "제한", + "limit-hint": "최소 제한 값은 2, 최대는 1000입니다. 단일 항목을 가져오려면 가져오기 모드 '첫 번째' 또는 '마지막'을 선택하세요.", + "limit-required": "제한이 필요합니다.", + "limit-range": "제한은 2에서 1000 사이여야 합니다.", + "time-unit-milliseconds": "밀리초", + "time-unit-seconds": "초", + "time-unit-minutes": "분", + "time-unit-hours": "시간", + "time-unit-days": "요일", + "time-value-range": "1~2147483647 범위를 허용합니다.", + "start-interval-value-required": "간격 시작은 필수 항목입니다.", + "end-interval-value-required": "간격 종료는 필수 항목입니다.", + "filter": "필터", + "switch": "스위치", + "math-templatization-tooltip": "이 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "add-message-type": "메시지 유형 추가", + "select-message-types-required": "최소 하나의 메시지 유형을 선택해야 합니다.", + "select-message-types": "메시지 유형 선택", + "no-message-types-found": "메시지 유형을 찾을 수 없음", + "no-message-type-matching": "'{{messageType}}'을(를) 찾을 수 없습니다.", + "create-new-message-type": "새로 만들기", + "message-types-required": "메시지 유형이 필요합니다.", + "client-attributes": "클라이언트 속성", + "shared-attributes": "공유 속성", + "server-attributes": "서버 속성", + "attributes-keys": "속성 키", + "attributes-keys-required": "속성 키가 필요합니다", + "attributes-scope": "속성 범위", + "attributes-scope-value": "속성 범위 값", + "attributes-scope-value-copy": "속성 범위 값 복사", + "attributes-scope-hint": "'scope' 메타데이터 키를 사용하여 메시지별로 속성 범위를 동적으로 설정하세요. 제공된 경우 구성에 설정된 범위를 재정의합니다.", + "notify-device": "장치에 알림 강제 전송", + "send-attributes-updated-notification": "속성 업데이트 알림 전송", + "send-attributes-updated-notification-hint": "업데이트된 속성에 대한 알림을 별도의 메시지로 규칙 엔진 큐에 보냅니다.", + "send-attributes-deleted-notification": "속성 삭제 알림 보내기", + "send-attributes-deleted-notification-hint": "삭제된 속성에 대한 알림을 별도의 메시지로 규칙 엔진 큐에 보냅니다.", + "update-attributes-only-on-value-change": "값이 변경된 경우에만 속성 저장", + "update-attributes-only-on-value-change-hint": "값이 변경되었는지 여부에 관계없이 모든 수신 메시지에서 속성을 업데이트합니다. API 사용량이 증가하고 성능이 저하됩니다.", + "update-attributes-only-on-value-change-hint-enabled": "값이 변경된 경우에만 속성을 업데이트합니다. 값이 변경되지 않으면 속성 타임스탬프 업데이트나 속성 변경 알림이 전송되지 않습니다.", + "fetch-credentials-to-metadata": "메타데이터에 자격 증명 가져오기", + "notify-device-on-update-hint": "활성화하면 공유 속성 업데이트에 대한 알림을 장치에 강제로 보냅니다. 비활성화하면 알림 동작이 수신 메시지 메타데이터의 'notifyDevice' 매개변수에 의해 제어됩니다. 알림을 끄려면 메시지 메타데이터에 'notifyDevice' 매개변수가 'false'로 설정되어 있어야 합니다. 다른 모든 경우에는 장치에 알림이 전송됩니다.", + "notify-device-on-delete-hint": "활성화하면 공유 속성 제거에 대한 알림을 장치에 강제로 보냅니다. 비활성화하면 알림 동작이 수신 메시지 메타데이터의 'notifyDevice' 매개변수에 의해 제어됩니다. 알림을 켜려면 메시지 메타데이터에 'notifyDevice' 매개변수가 'true'로 설정되어 있어야 합니다. 다른 모든 경우에는 장치에 알림이 전송되지 않습니다.", + "latest-timeseries": "최신 시계열 데이터 키", + "timeseries-keys": "시계열 키", + "timeseries-keys-required": "최소 하나의 시계열 키를 선택해야 합니다.", + "add-timeseries-key": "시계열 키 추가", + "add-message-field": "메시지 필드 추가", + "relation-search-parameters": "관계 검색 매개변수", + "relation-parameters": "관계 매개변수", + "add-metadata-field": "메타데이터 필드 추가", + "data-keys": "메시지 필드 이름", + "copy-from": "복사 원본", + "data-to-metadata": "데이터를 메타데이터로", + "metadata-to-data": "메타데이터를 데이터로", + "use-regular-expression-hint": "정규 표현식을 사용하여 패턴별로 키를 복사하세요.\n\n팁 & 요령:\n'Enter'를 눌러 필드 이름 입력을 완료하세요.\n'Backspace'를 눌러 필드 이름을 삭제하세요. 여러 필드 이름이 지원됩니다.", + "interval": "간격", + "interval-required": "간격이 필요합니다", + "interval-hint": "중복 제거 간격 (초).", + "interval-min-error": "최소 허용 값은 1입니다", + "max-pending-msgs": "보류 중인 최대 메시지 수", + "max-pending-msgs-hint": "각 고유 중복 제거 ID에 대해 메모리에 저장되는 최대 메시지 수입니다.", + "max-pending-msgs-required": "보류 중인 최대 메시지 수가 필요합니다", + "max-pending-msgs-max-error": "최대 허용 값은 1000입니다", + "max-pending-msgs-min-error": "최소 허용 값은 1입니다", + "max-retries": "최대 재시도 횟수", + "max-retries-required": "최대 재시도 횟수가 필요합니다", + "max-retries-hint": "중복 제거된 메시지를 큐에 푸시하기 위한 최대 재시도 횟수입니다. 재시도 사이에 10초 지연이 사용됩니다", + "max-retries-max-error": "최대 허용 값은 100입니다", + "max-retries-min-error": "최소 허용 값은 0입니다", + "strategy": "전략", + "strategy-required": "전략이 필요합니다", + "strategy-all-hint": "중복 제거 기간 동안 도착한 모든 메시지를 단일 JSON 배열 메시지로 반환합니다. 각 요소는 msg 및 metadata 내부 속성이 있는 엔티티를 나타냅니다.", + "strategy-first-hint": "중복 제거 기간 동안 도착한 첫 번째 메시지를 반환합니다.", + "strategy-last-hint": "중복 제거 기간 동안 도착한 마지막 메시지를 반환합니다.", + "first": "첫 번째", + "last": "과거", + "all": "모두", + "output-msg-type-hint": "중복 제거 결과의 메시지 유형입니다.", + "queue-name-hint": "중복 제거 결과가 게시될 큐 이름입니다.", + "keys": "키", + "keys-required": "키가 필요합니다", + "rename-keys-in": "키 이름 변경 위치", + "data": "데이터", + "message": "메시지", + "metadata": "메타데이터", + "current-key-name": "현재 키 이름", + "key-name-required": "키 이름은 필수 항목입니다.", + "new-key-name": "새 키 이름", + "new-key-name-required": "새 키 이름이 필요합니다", + "metadata-keys": "메타데이터 필드 이름", + "json-path-expression": "JSON 경로 표현식", + "json-path-expression-required": "JSON 경로 표현식이 필요합니다", + "json-path-expression-hint": "JSONPath는 JSON 구조에서 요소 또는 요소 집합의 경로를 지정합니다. '$'는 루트 엔티티 또는 배열을 나타냅니다.", + "relations-query": "관계 질의", + "device-relations-query": "장치 관계 쿼리", + "max-relation-level": "최대 관계 수준", + "max-relation-level-error": "값은 0보다 크거나 지정되지 않아야 합니다.", + "max-relation-level-invalid": "값은 정수여야 합니다.", + "relation-type": "관계 유형", + "relation-type-pattern": "관계 유형 패턴", + "relation-type-pattern-required": "관계 유형 패턴이 필요합니다", + "relation-types-list": "전파할 관계 유형", + "relation-types-list-hint": "전파 관계 유형이 선택되지 않으면 관계 유형별 필터링 없이 알람이 전파됩니다.", + "unlimited-level": "제한 수준", + "latest-telemetry": "최근 데이터", + "add-telemetry-key": "텔레메트리 키 추가", + "delete-from": "삭제 위치", + "use-regular-expression-delete-hint": "정규 표현식을 사용하여 패턴별로 키를 삭제하세요.\n\n팁 & 요령:\n'Enter'를 눌러 필드 이름 입력을 완료하세요.\n'Backspace'를 눌러 필드 이름을 삭제하세요.\n여러 필드 이름이 지원됩니다.", + "fetch-into": "가져오기 위치", + "attr-mapping": "속성 매핑:", + "source-attribute": "소스 속성 키", + "source-attribute-required": "소스 속성 키가 필요합니다.", + "source-telemetry": "소스 텔레메트리 키", + "source-telemetry-required": "소스 텔레메트리 키가 필요합니다.", + "target-key": "대상 키", + "target-key-required": "대상 키가 필요합니다.", + "attr-mapping-required": "최소 하나의 매핑 항목을 지정해야 합니다.", + "fields-mapping": "필드 매핑", + "fields-mapping-hint": "메시지 필드가 $entityId로 설정된 경우 메시지 발신자의 ID가 지정된 테이블 열에 저장됩니다.", + "relations-query-config-direction-suffix": "발신자", + "profile-name": "프로필 이름", + "fetch-circle-parameter-info-from-metadata-hint": "메타데이터 필드 '{{perimeterKeyName}}'은(는) 다음 형식으로 정의되어야 합니다: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}", + "fetch-poligon-parameter-info-from-metadata-hint": "메타데이터 필드 '{{perimeterKeyName}}'은(는) 다음 형식으로 정의되어야 합니다: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]", + "short-templatization-tooltip": "$[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "fields-mapping-required": "최소 하나의 필드 매핑을 지정해야 합니다.", + "at-least-one-field-required": "최소 하나의 입력 필드에 값이 제공되어야 합니다.", + "originator-fields-sv-map-hint": "대상 키 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "sv-map-hint": "대상 키 필드만 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "source-field": "소스 필드", + "source-field-required": "소스 필드가 필요합니다.", + "originator-source": "발신자 소스", + "new-originator": "새 발신자", + "originator-customer": "고객", + "originator-tenant": "테넌트", + "originator-related": "관련 엔티티", + "originator-alarm-originator": "알람 발생자", + "originator-entity": "이름 패턴으로 엔티티 찾기", + "clone-message": "메시지 복제", + "transform": "변환", + "default-ttl": "기본 TTL", + "default-ttl-required": "기본 TTL은 필수 항목입니다.", + "default-ttl-hint": "규칙 노드는 메시지 메타데이터에서 TTL(Time-to-Live) 값을 가져옵니다. 값이 없으면 구성에 지정된 TTL이 기본값이 됩니다. 값이 0으로 설정되면 테넌트 프로필 구성의 TTL이 적용됩니다.", + "default-ttl-zero-hint": "값이 0이면 TTL이 적용되지 않습니다.", + "min-default-ttl-message": "최소 TTL은 0만 허용됩니다.", + "generation-parameters": "생성 매개변수", + "message-count": "생성 메시지 제한 (0 - 무제한)", + "message-count-required": "생성 메시지 제한은 필수 항목입니다.", + "min-message-count-message": "최소 메시지 수는 0만 허용됩니다.", + "period-seconds": "기간 (초)", + "period-seconds-required": "기간은 필수 항목입니다.", + "generation-frequency-seconds": "생성 빈도 (초)", + "generation-frequency-required": "생성 빈도는 필수 항목입니다.", + "min-generation-frequency-message": "최소 1초만 허용됩니다.", + "script-lang-tbel": "TBEL", + "script-lang-js": "JS", + "use-metadata-period-in-seconds-patterns": "초 단위 기간 패턴 사용", + "use-metadata-period-in-seconds-patterns-hint": "선택하면 규칙 노드는 간격이 초 단위라고 가정하여 메시지 메타데이터 또는 데이터의 초 간격 패턴을 사용합니다.", + "period-in-seconds-pattern": "초 단위 기간 패턴", + "period-in-seconds-pattern-required": "초 단위 기간 패턴은 필수 항목입니다.", + "min-period-seconds-message": "최소 기간은 1초만 허용됩니다.", + "originator": "만든 사람", + "message-body": "메시지 본문", + "message-metadata": "메시지 메타데이터", + "generate": "생성", + "current-rule-node": "현재 규칙 노드", + "current-tenant": "현재 테넌트", + "generator-function": "생성기 함수", + "test-generator-function": "생성기 함수 테스트", + "generator": "생성기", + "test-filter-function": "필터 함수 테스트", + "test-switch-function": "스위치 함수 테스트", + "test-transformer-function": "변환기 함수 테스트", + "transformer": "변환기", + "alarm-create-condition": "알람 생성 조건", + "test-condition-function": "조건 함수 테스트", + "alarm-clear-condition": "알람 해제 조건", + "alarm-details-builder": "알람 상세 빌더", + "test-details-function": "세부 정보 함수 테스트", + "alarm-type": "알람 유형", + "select-entity-types": "엔티티 유형 선택", + "alarm-type-required": "알람 유형은 필수 항목입니다.", + "alarm-severity": "알람 심각도", + "alarm-severity-required": "알람 심각도가 필요합니다", + "alarm-severity-pattern": "알람 심각도 패턴", + "alarm-status-filter": "알람 상태 필터", + "alarm-status-list-empty": "알람 상태 목록이 비어 있습니다", + "no-alarm-status-matching": "일치하는 알람 상태를 찾을 수 없습니다.", + "propagate": "관련 엔티티에 알람 전파", + "propagate-to-owner": "엔티티 소유자(고객 또는 테넌트)에게 알람 전파", + "propagate-to-tenant": "테넌트에게 알람 전파", + "condition": "조건", + "details": "상세 내역", + "to-string": "문자열로 변환", + "test-to-string-function": "문자열 변환 함수 테스트", + "from-template": "부터", + "from-template-required": "보낸 사람은 필수 항목입니다.", + "message-to-metadata": "메시지에서 메타데이터로", + "metadata-to-message": "메타데이터에서 메시지로", + "from-message": "메시지에서", + "from-metadata": "메타데이터에서", + "to-template": "받는 사람", + "to-template-required": "받는 사람 템플릿은 필수 항목입니다.", + "mail-address-list-template-hint": "쉼표로 구분된 주소 목록, 메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요", + "cc-template": "참조", + "bcc-template": "숨은 참조", + "subject-template": "제목", + "subject-template-required": "제목 템플릿은 필수 항목입니다.", + "body-template": "본문", + "body-template-required": "본문 템플릿은 필수 항목입니다.", + "dynamic-mail-body-type": "동적 메일 본문 유형", + "mail-body-type": "메일 본문 유형", + "body-type-template": "본문 유형 템플릿", + "reply-routing-configuration": "응답 라우팅 구성", + "rpc-reply-routing-configuration-hint": "이 구성 매개변수는 응답을 보내기 위해 서비스, 세션 및 요청을 식별하는 데 사용되는 메타데이터 키 이름을 지정합니다.", + "reply-routing-configuration-hint": "이 구성 매개변수는 응답을 보내기 위해 서비스와 요청을 식별하는 데 사용되는 메타데이터 키 이름을 지정합니다.", + "request-id-metadata-attribute": "요청 ID", + "service-id-metadata-attribute": "서비스 ID", + "session-id-metadata-attribute": "세션 ID", + "timeout-sec": "시간 초과 (초)", + "timeout-required": "시간 초과는 필수 항목입니다.", + "min-timeout-message": "최소 시간 초과 값은 0만 허용됩니다.", + "endpoint-url-pattern": "엔드포인트 URL 패턴", + "endpoint-url-pattern-required": "엔드포인트 URL 패턴은 필수 항목입니다.", + "request-method": "요청 방법", + "use-simple-client-http-factory": "간단한 클라이언트 HTTP 팩토리 사용", + "ignore-request-body": "요청 본문 없음", + "parse-to-plain-text": "일반 텍스트로 파싱", + "parse-to-plain-text-hint": "선택하면 요청 본문 메시지 페이로드가 JSON 문자열에서 일반 텍스트로 변환됩니다. 예: msg = \"Hello,\\t\"world\"\"는 Hello, \"world\"로 구문 분석됩니다.", + "read-timeout": "읽기 시간 초과 (밀리초)", + "read-timeout-hint": "0 값은 무한 시간 초과를 의미합니다.", + "max-parallel-requests-count": "최대 병렬 요청 수", + "max-parallel-requests-count-hint": "0 값은 병렬 처리에 제한이 없음을 지정합니다.", + "max-response-size": "최대 응답 크기 (KB)", + "max-response-size-hint": "JSON 또는 XML 페이로드와 같은 HTTP 메시지를 디코딩하거나 인코딩할 때 데이터 버퍼링에 할당된 최대 메모리 양", + "headers": "헤더", + "headers-hint": "헤더/값 필드에서 메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요", + "header": "헤더", + "header-required": "헤더는 필수 항목입니다.", + "value": "값", + "value-required": "값은 필수 항목입니다.", + "topic-pattern": "토픽 패턴", + "key-pattern": "키 패턴", + "key-pattern-hint": "선택 사항. 유효한 파티션 번호가 지정되면 레코드를 보낼 때 사용됩니다. 파티션이 지정되지 않으면 키가 대신 사용됩니다. 둘 다 지정되지 않으면 라운드 로빈 방식으로 파티션이 할당됩니다.", + "topic-pattern-required": "토픽 패턴은 필수 항목입니다.", + "topic": "토픽", + "topic-required": "토픽은 필수 항목입니다.", + "bootstrap-servers": "부트스트랩 서버", + "bootstrap-servers-required": "부트스트랩 서버 값이 필요합니다", + "other-properties": "기타 속성", + "key": "키", + "key-required": "키는 필수 항목입니다.", + "retries": "실패 시 자동 재시도 횟수", + "min-retries-message": "최소 재시도는 0만 허용됩니다.", + "batch-size-bytes": "생성된 배치 크기 (바이트)", + "min-batch-size-bytes-message": "최소 배치 크기는 0만 허용됩니다.", + "linger-ms": "로컬 버퍼링 시간 (ms)", + "min-linger-ms-message": "최소 0ms만 허용됩니다.", + "buffer-memory-bytes": "클라이언트 버퍼 최대 크기 (바이트)", + "min-buffer-memory-message": "최소 버퍼 크기는 0만 허용됩니다.", + "memory-buffer-size-range": "메모리 버퍼 크기는 0에서 {{max}} KB 사이여야 합니다.", + "acks": "확인 수", + "topic-arn-pattern": "토픽 ARN 패턴", + "topic-arn-pattern-required": "토픽 ARN 패턴은 필수 항목입니다.", + "aws-access-key-id": "AWS 액세스 키 ID", + "aws-access-key-id-required": "AWS 액세스 ID를 입력하세요.", + "aws-secret-access-key": "AWS 보안 액세스 키", + "aws-secret-access-key-required": "AWS 보안 액세스 키를 입력하세요", + "aws-region": "AWS 지역", + "aws-region-required": "AWS 지역을 입력하세요", + "exchange-name-pattern": "교환 이름 패턴", + "routing-key-pattern": "라우팅 키 패턴", + "message-properties": "메시지 속성", + "host": "호스트", + "host-required": "호스트는 필수 항목입니다.", + "port": "포트", + "port-required": "포트는 필수 항목입니다.", + "port-range": "포트를 1~65535 범위 내에서 입력하세요.", + "virtual-host": "가상 호스트", + "username": "사용자명", + "password": "비밀번호", + "automatic-recovery": "자동 복구", + "connection-timeout-ms": "연결 시간 초과 (ms)", + "min-connection-timeout-ms-message": "최소 0ms만 허용됩니다.", + "handshake-timeout-ms": "핸드셰이크 시간 초과 (ms)", + "min-handshake-timeout-ms-message": "최소 0ms만 허용됩니다.", + "client-properties": "클라이언트 속성", + "queue-url-pattern": "큐 URL 패턴", + "queue-url-pattern-required": "큐 URL 패턴은 필수 항목입니다.", + "delay-seconds": "지연 (초)", + "min-delay-seconds-message": "최소 0초만 허용됩니다.", + "max-delay-seconds-message": "최대 900초만 허용됩니다.", + "name": "이름", + "name-required": "이름은 필수 항목입니다.", + "queue-type": "큐 유형", + "sqs-queue-standard": "표준", + "sqs-queue-fifo": "FIFO", + "gcp-project-id": "GCP 프로젝트 ID", + "gcp-project-id-required": "GCP 프로젝트 ID는 필수 항목입니다.", + "gcp-service-account-key": "GCP 서비스 계정 키 파일", + "gcp-service-account-key-required": "GCP 서비스 계정 키 파일은 필수 항목입니다.", + "pubsub-topic-name": "토픽 이름", + "pubsub-topic-name-required": "토픽 이름은 필수 항목입니다.", + "message-attributes": "메시지 속성", + "message-attributes-hint": "이름/값 필드에서 메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요", + "connect-timeout": "연결 시간 초과 (초)", + "connect-timeout-required": "연결 시간 초과는 필수 항목입니다.", + "connect-timeout-range": "연결 시간 초과는 1에서 200 범위여야 합니다.", + "client-id": "클라이언트 ID", + "client-id-hint": "선택 사항. 자동 생성된 클라이언트 ID를 사용하려면 비워 두세요. 클라이언트 ID를 지정할 때 주의하세요. 대부분의 MQTT 브로커는 동일한 클라이언트 ID로 여러 연결을 허용하지 않습니다. 이러한 브로커에 연결하려면 MQTT 클라이언트 ID가 고유해야 합니다. 플랫폼이 마이크로 서비스 모드로 실행 중인 경우 각 마이크로 서비스에서 규칙 노드의 복사본이 시작됩니다. 이로 인해 동일한 ID를 가진 여러 MQTT 클라이언트가 자동으로 생성되어 규칙 노드에 오류가 발생할 수 있습니다. 이러한 오류를 방지하려면 아래의 \"서비스 ID를 클라이언트 ID에 접미사로 추가\" 옵션을 활성화하세요.", + "append-client-id-suffix": "클라이언트 ID에 서비스 ID를 접미사로 추가", + "client-id-suffix-hint": "선택 사항. \"클라이언트 ID\"가 명시적으로 지정된 경우에 적용됩니다. 선택하면 서비스 ID가 클라이언트 ID에 접미사로 추가됩니다. 플랫폼이 마이크로 서비스 모드로 실행 중일 때 오류를 방지하는 데 도움이 됩니다.", + "device-id": "장치 ID", + "device-id-required": "장치 ID는 필수 항목입니다.", + "clean-session": "클린 세션", + "enable-ssl": "SSL 활성화", + "credentials": "자격 증명", + "credentials-type": "자격 증명 타입", + "credentials-type-required": "자격 증명 유형은 필수 항목입니다.", + "credentials-anonymous": "익명", + "credentials-basic": "기본", + "credentials-pem": "PEM", + "credentials-pem-hint": "최소 서버 CA 인증서 파일 또는 클라이언트 인증서와 클라이언트 개인 키 파일 쌍이 필요합니다", + "credentials-sas": "공유 액세스 서명", + "sas-key": "SAS 키", + "sas-key-required": "SAS 키는 필수 항목입니다.", + "hostname": "호스트 이름", + "hostname-required": "호스트 이름은 필수 항목입니다.", + "azure-ca-cert": "CA 인증서 파일", + "username-required": "사용자 이름이 필요합니다.", + "password-required": "비밀번호가 필요합니다.", + "ca-cert": "서버 CA 인증서 파일", + "private-key": "클라이언트 개인 키 파일", + "cert": "클라이언트 인증서 파일", + "no-file": "아무 파일도 선택되지 않았습니다.", + "drop-file": "여기로 파일을 드래그 하거나, 여기를 클릭해서 파일을 선택하세요.", + "private-key-password": "개인 키 비밀번호", + "use-system-smtp-settings": "시스템 SMTP 설정 사용", + "use-metadata-dynamic-interval": "동적 간격 사용", + "metadata-dynamic-interval-hint": "간격 시작 및 종료 입력 필드는 템플릿을 지원합니다. 대체된 템플릿 값은 밀리초로 설정되어야 합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "use-metadata-interval-patterns-hint": "선택하면 규칙 노드는 간격이 밀리초라고 가정하여 메시지 메타데이터 또는 데이터의 시작 및 종료 간격 패턴을 사용합니다.", + "use-message-alarm-data": "메시지 알람 데이터 사용", + "overwrite-alarm-details": "알람 세부 정보 덮어쓰기", + "use-alarm-severity-pattern": "알람 심각도 패턴 사용", + "check-all-keys": "모든 지정된 필드가 있는지 확인", + "check-all-keys-hint": "선택하면 지정된 모든 키가 메시지 데이터와 메타데이터에 있는지 확인합니다.", + "check-relation-to-specific-entity": "특정 엔티티와의 관계 확인", + "check-relation-to-specific-entity-tooltip": "활성화하면 특정 엔티티와의 관계 존재 여부를 확인하고, 그렇지 않으면 모든 엔티티와의 관계 존재 여부를 확인합니다. 두 경우 모두 관계 조회는 구성된 방향과 유형을 기반으로 합니다.", + "check-relation-hint": "방향과 관계 유형을 기반으로 특정 엔티티 또는 모든 엔티티와의 관계 존재 여부를 확인합니다.", + "delete-relation-with-specific-entity": "특정 엔티티와의 관계 삭제", + "delete-relation-with-specific-entity-hint": "활성화하면 하나의 특정 엔티티와의 관계만 삭제됩니다. 그렇지 않으면 일치하는 모든 엔티티와의 관계가 제거됩니다.", + "delete-relation-hint": "수신 메시지의 발신자에서 지정된 엔티티 또는 엔티티 목록으로의 관계를 방향과 유형에 따라 삭제합니다.", + "remove-current-relations": "현재 관계 제거", + "remove-current-relations-hint": "수신 메시지의 발신자에서 방향과 유형에 따라 현재 관계를 제거합니다.", + "change-originator-to-related-entity": "발신자를 관련 엔티티로 변경", + "change-originator-to-related-entity-hint": "제출된 메시지를 다른 엔티티의 메시지로 처리하는 데 사용됩니다.", + "start-interval": "간격 시작", + "end-interval": "간격 종료", + "start-interval-required": "간격 시작은 필수 항목입니다.", + "end-interval-required": "간격 종료는 필수 항목입니다.", + "smtp-protocol": "프로토콜", + "smtp-host": "SMTP 호스트", + "smtp-host-required": "SMTP 호스트를 입력해야 합니다.", + "smtp-port": "SMTP 포트", + "smtp-port-required": "SMTP 포트를 입력해야 합니다.", + "smtp-port-range": "SMTP 포트는 1에서 65535 범위여야 합니다.", + "timeout-msec": "시간 초과 (ms)", + "min-timeout-msec-message": "최소 0ms만 허용됩니다.", + "enter-username": "사용자명을 입력하세요.", + "enter-password": "비밀번호를 입력하세요.", + "enable-tls": "TLS 사용", + "tls-version": "TLS 버전", + "enable-proxy": "프록시 사용", + "use-system-proxy-properties": "시스템 프록시 속성 사용", + "proxy-host": "프록시 호스트", + "proxy-host-required": "프록시 호스트를 입력하세요.", + "proxy-port": "프록시 포트", + "proxy-port-required": "프록시 포트를 입력하세요.", + "proxy-port-range": "프록시 포트를 1~65535 범위 내에서 입력하세요.", + "proxy-user": "프록시 사용자", + "proxy-password": "프록시 비밀번호", + "proxy-scheme": "프록시 스키마", + "numbers-to-template": "받는 사람 전화번호 템플릿", + "numbers-to-template-required": "받는 사람 전화번호 템플릿은 필수 항목입니다.", + "numbers-to-template-hint": "쉼표로 구분된 전화번호, 메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요", + "sms-message-template": "SMS 메시지 템플릿", + "sms-message-template-required": "SMS 메시지 템플릿은 필수 항목입니다.", + "use-system-sms-settings": "시스템 SMS 제공자 설정 사용", + "min-period-0-seconds-message": "최소 기간은 0초만 허용됩니다.", + "max-pending-messages": "최대 대기 메시지", + "max-pending-messages-required": "최대 대기 메시지는 필수 항목입니다.", + "max-pending-messages-range": "최대 대기 메시지는 1에서 100000 범위여야 합니다.", + "originator-types-filter": "발신자 유형 필터", + "interval-seconds": "간격 (초)", + "interval-seconds-required": "간격은 필수 항목입니다.", + "int-range": "값은 최대 정수 제한(2147483648)을 초과하지 않아야 합니다.", + "min-interval-seconds-message": "최소 간격은 1초만 허용됩니다.", + "output-timeseries-key-prefix": "출력 시계열 키 접두사", + "output-timeseries-key-prefix-required": "출력 시계열 키 접두사는 필수 항목입니다.", + "separator-hint": "\"Enter\"를 눌러 필드 입력을 완료하세요.", + "select-details": "세부 정보 선택", + "entity-details-id": "ID", + "entity-details-title": "제목", + "entity-details-country": "국가", + "entity-details-state": "주", + "entity-details-city": "시", + "entity-details-zip": "우편번호", + "entity-details-address": "주소", + "entity-details-address2": "상세 주소", + "entity-details-additional_info": "추가 정보", + "entity-details-phone": "전화번호", + "entity-details-email": "이메일", + "email-sender": "이메일 보낸 사람", + "fields-to-check": "확인할 필드", + "add-detail": "상세 추가", + "check-all-keys-tooltip": "활성화하면 수신 메시지와 해당 메타데이터 내의 메시지 및 메타데이터 필드 이름에 나열된 모든 필드의 존재 여부를 확인합니다.", + "fields-to-check-hint": "\"Enter\"를 눌러 필드 이름 입력을 완료하세요. 여러 필드 이름이 지원됩니다.", + "entity-details-list-empty": "최소 하나의 세부 정보를 선택해야 합니다.", + "alarm-status": "알람 상태", + "alarm-required": "최소 하나의 알람 상태를 선택해야 합니다.", + "no-entity-details-matching": "일치하는 엔티티 세부 정보를 찾을 수 없습니다.", + "custom-table-name": "사용자 지정 테이블 이름", + "custom-table-name-required": "테이블 이름이 필요합니다", + "custom-table-hint": "테이블은 Cassandra 클러스터에 생성되어야 하며 일반 TB 테이블에 데이터가 삽입되지 않도록 이름이 'cs_tb_' 접두사로 시작해야 합니다. 여기에 'cs_tb_' 접두사 없이 테이블 이름을 입력하세요.", + "message-field": "메시지 필드", + "message-field-required": "메시지 필드가 필요합니다.", + "table-col": "테이블 열", + "table-col-required": "테이블 열이 필요합니다.", + "latitude-field-name": "위도 필드 이름", + "longitude-field-name": "경도 필드 이름", + "latitude-field-name-required": "위도 필드 이름이 필요합니다.", + "longitude-field-name-required": "경도 필드 이름이 필요합니다.", + "fetch-perimeter-info-from-metadata": "메타데이터에서 경계 정보 가져오기", + "fetch-perimeter-info-from-metadata-tooltip": "경계 유형이 '다각형'으로 설정된 경우 메타데이터 필드 '{{perimeterKeyName}}'의 값이 추가 구문 분석 없이 경계 정의로 설정됩니다. 그렇지 않고 경계 유형이 '원'으로 설정된 경우 '{{perimeterKeyName}}' 메타데이터 필드의 값이 구문 분석되어 원 경계 정의에 사용되는 'latitude', 'longitude', 'radius', 'radiusUnit' 필드가 추출됩니다.", + "perimeter-key-name": "경계 키 이름", + "perimeter-key-name-hint": "경계 정보를 포함하는 메타데이터 필드 이름입니다.", + "perimeter-key-name-required": "경계 키 이름이 필요합니다.", + "perimeter-circle": "원", + "perimeter-polygon": "다각형", + "perimeter-type": "경계 유형", + "circle-center-latitude": "중심 위도", + "circle-center-latitude-required": "중심 위도가 필요합니다.", + "circle-center-longitude": "중심 경도", + "circle-center-longitude-required": "중심 경도가 필요합니다.", + "range-unit-meter": "미터", + "range-unit-kilometer": "킬로미터", + "range-unit-foot": "피트", + "range-unit-mile": "마일", + "range-unit-nautical-mile": "해리", + "range-units": "범위 단위", + "range-units-required": "범위 단위가 필요합니다.", + "range": "범위", + "range-required": "범위가 필요합니다.", + "polygon-definition": "다각형 정의", + "polygon-definition-required": "다각형 정의가 필요합니다.", + "polygon-definition-hint": "다각형을 수동으로 정의하려면 다음 형식을 사용하세요: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].", + "min-inside-duration": "최소 내부 지속 시간", + "min-inside-duration-value-required": "최소 내부 지속 시간이 필요합니다", + "min-inside-duration-time-unit": "최소 내부 지속 시간 단위", + "min-outside-duration": "최소 외부 지속 시간", + "min-outside-duration-value-required": "최소 외부 지속 시간이 필요합니다", + "min-outside-duration-time-unit": "최소 외부 지속 시간 단위", + "tell-failure-if-absent": "실패 알림", + "tell-failure-if-absent-hint": "선택한 키 중 하나라도 존재하지 않으면 아웃바운드 메시지가 \"실패\"를 보고합니다.", + "get-latest-value-with-ts": "최신 텔레메트리 값에 대한 타임스탬프 가져오기", + "get-latest-value-with-ts-hint": "선택하면 최신 텔레메트리 값에 타임스탬프도 포함됩니다. 예: \"temp\": \"{\"ts\":1574329385897, \"value\":42}\"", + "ignore-null-strings": "빈 문자열 무시", + "ignore-null-strings-hint": "선택하면 규칙 노드가 빈 값이 있는 엔티티 필드를 무시합니다.", + "add-metadata-key-values-as-kafka-headers": "Kafka 레코드 헤더에 메시지 메타데이터 키-값 쌍 추가", + "add-metadata-key-values-as-kafka-headers-hint": "선택하면 메시지 메타데이터의 키-값 쌍이 미리 정의된 문자셋 인코딩으로 바이트 배열로 나가는 레코드 헤더에 추가됩니다.", + "charset-encoding": "문자셋 인코딩", + "charset-encoding-required": "문자셋 인코딩이 필요합니다.", + "charset-us-ascii": "US-ASCII", + "charset-iso-8859-1": "ISO-8859-1", + "charset-utf-8": "UTF-8", + "charset-utf-16be": "UTF-16BE", + "charset-utf-16le": "UTF-16LE", + "charset-utf-16": "UTF-16", + "select-queue-hint": "큐 이름은 드롭다운 목록에서 선택하거나 사용자 지정 이름을 추가할 수 있습니다.", + "device-profile-node-hint": "알람 상태 평가의 연속성을 보장하기 위해 지속 시간 또는 반복 조건이 있는 경우에 유용합니다.", + "persist-alarm-rules": "알람 규칙 상태 저장", + "persist-alarm-rules-hint": "활성화하면 규칙 노드가 처리 상태를 데이터베이스에 저장합니다.", + "fetch-alarm-rules": "알람 규칙 상태 가져오기", + "fetch-alarm-rules-hint": "활성화하면 규칙 노드가 초기화 시 처리 상태를 복원하고 서버가 다시 시작된 후에도 알람이 발생하도록 합니다. 그렇지 않으면 장치에서 첫 번째 메시지가 도착할 때 상태가 복원됩니다.", + "input-value-key": "입력 값 키", + "input-value-key-required": "입력 값 키가 필요합니다.", + "output-value-key": "출력 값 키", + "output-value-key-required": "출력 값 키가 필요합니다.", + "number-of-digits-after-floating-point": "소숫점 뒤 자릿수", + "number-of-digits-after-floating-point-range": "소수점 이하 자릿수는 0에서 15 사이여야 합니다.", + "failure-if-delta-negative": "델타가 음수인 경우 실패 알림", + "failure-if-delta-negative-tooltip": "규칙 노드는 델타 값이 음수인 경우 메시지 처리 실패를 강제합니다.", + "use-caching": "캐싱 사용", + "use-caching-tooltip": "규칙 노드는 성능을 향상시키기 위해 수신 메시지에서 도착하는 \"{{inputValueKey}}\"의 값을 캐시합니다. 다른 곳에서 \"{{inputValueKey}}\" 값을 수정하면 캐시가 업데이트되지 않습니다.", + "add-time-difference-between-readings": "\"{{inputValueKey}}\" 판독 간의 시간 차이 추가", + "add-time-difference-between-readings-tooltip": "활성화하면 규칙 노드가 \"{{periodValueKey}}\"를 아웃바운드 메시지에 추가합니다.", + "period-value-key": "기간 값 키", + "period-value-key-required": "기간 값 키가 필요합니다.", + "general-pattern-hint": "메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요.", + "alarm-severity-pattern-hint": "메타데이터의 값에는 ${metadataKey}를 사용하고, 메시지 본문의 값에는 $[messageKey]를 사용하세요. 알람 심각도는 시스템(CRITICAL, MAJOR 등)이어야 합니다.", + "output-node-name-hint": "규칙 노드 이름은 출력 메시지의 관계 유형에 해당하며, 호출자 규칙 체인의 다른 규칙 노드로 메시지를 전달하는 데 사용됩니다.", + "use-server-ts": "서버 타임스탬프 사용", + "use-server-ts-hint": "명시적 타임스탬프가 없는 시계열 데이터에 대해 서버의 현재 타임스탬프를 사용합니다. 이는 여러 소스에서 메시지를 처리하거나 메시지가 순서 없이 도착할 때 적절한 순서를 유지하는 데 도움이 됩니다.", + "kv-map-pattern-hint": "모든 입력 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "kv-map-single-pattern-hint": "입력 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "shared-scope": "공유 범위", + "server-scope": "서버 범위", + "client-scope": "클라이언트 범위", + "attribute-type": "속성", + "attribute-type-description": "데이터베이스에서 속성 값 가져오기", + "attribute-type-result-description": "결과를 데이터베이스의 엔티티 속성으로 저장", + "constant-type": "상수", + "constant-type-description": "상수 값 정의", + "time-series-type": "시계열", + "time-series-type-description": "데이터베이스에서 최신 시계열 값 가져오기", + "time-series-type-result-description": "결과를 데이터베이스의 엔티티 시계열로 저장", + "message-body-type": "메시지", + "message-body-type-description": "수신 메시지에서 인수 값 가져오기", + "message-body-type-result-description": "아웃바운드 메시지에 결과 추가", + "message-metadata-type": "메타데이터", + "message-metadata-type-description": "수신 메시지 메타데이터에서 인수 값 가져오기", + "message-metadata-result-description": "아웃바운드 메시지 메타데이터에 결과 추가", + "argument-tile": "인수", + "no-arguments-prompt": "구성된 인수 없음", + "result-title": "결과", + "functions-field-input": "함수", + "no-option-found": "옵션을 찾을 수 없음", + "argument-source-field-input": "소스", + "argument-source-field-input-required": "인수 소스가 필요합니다.", + "argument-key-field-input": "키", + "argument-key-field-input-required": "인수 키가 필요합니다.", + "constant-value-field-input": "상수 값", + "constant-value-field-input-required": "상수 값이 필요합니다.", + "attribute-scope-field-input": "속성 범위", + "attribute-scope-field-input-required": "속성 범위가 필요합니다.", + "default-value-field-input": "기본값", + "type-field-input": "유형", + "type-field-input-required": "유형일 입력하세요.", + "key-field-input": "키", + "add-entity-type": "엔티티 유형 추가", + "add-device-profile": "장치 프로필 추가", + "key-field-input-required": "키는 필수 항목입니다.", + "number-floating-point-field-input": "소숫점 뒤 자릿수", + "number-floating-point-field-input-hint": "결과를 정수로 변환하려면 0을 사용하세요", + "add-to-message-field-input": "메시지에 추가", + "add-to-metadata-field-input": "메타데이터에 추가", + "custom-expression-field-input": "수학 표현식", + "custom-expression-field-input-required": "수학 표현식이 필요합니다", + "custom-expression-field-input-hint": "평가할 수학 표현식을 지정하세요. 기본 표현식은 화씨를 섭씨로 변환하는 방법을 보여줍니다", + "retained-message": "보존됨", + "attributes-mapping": "속성 매핑", + "latest-telemetry-mapping": "최신 텔레메트리 매핑", + "add-mapped-attribute-to": "매핑된 속성 추가 위치", + "add-mapped-latest-telemetry-to": "매핑된 최신 텔레메트리 추가 위치", + "add-mapped-fields-to": "매핑된 필드 추가 위치", + "add-selected-details-to": "선택한 세부 정보 추가 위치", + "clear-selected-types": "선택된 유형 지우기", + "clear-selected-details": "선택된 상세 지우기", + "clear-selected-fields": "선택된 필드 지우기", + "clear-selected-keys": "선택된 키 지우기", + "geofence-configuration": "지오펜스 구성", + "coordinate-field-names": "좌표 필드 이름", + "coordinate-field-hint": "규칙 노드는 메시지에서 지정된 필드를 검색하려고 시도합니다. 필드가 없으면 메타데이터에서 조회합니다.", + "presence-monitoring-strategy": "존재 모니터링 전략", + "presence-monitoring-strategy-on-first-message": "첫 번째 메시지에서", + "presence-monitoring-strategy-on-each-message": "각 메시지에서", + "presence-monitoring-strategy-on-first-message-hint": "이전 존재 상태 '진입' 또는 '이탈' 업데이트 이후 구성된 최소 지속 시간이 지난 후 첫 번째 메시지에서 존재 상태 '내부' 또는 '외부'를 보고합니다.", + "presence-monitoring-strategy-on-each-message-hint": "존재 상태 '진입' 또는 '이탈' 업데이트 후 각 메시지에서 존재 상태 '내부' 또는 '외부'를 보고합니다.", + "fetch-credentials-to": "자격 증명 가져오기 위치", + "add-originator-attributes-to": "발신자 속성 추가 위치", + "originator-attributes": "발신자 속성", + "fetch-latest-telemetry-with-timestamp": "타임스탬프와 함께 최신 텔레메트리 가져오기", + "fetch-latest-telemetry-with-timestamp-tooltip": "선택하면 최신 텔레메트리 값이 타임스탬프와 함께 아웃바운드 메타데이터에 추가됩니다. 예: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"", + "tell-failure": "속성이 누락된 경우 실패 알림", + "tell-failure-tooltip": "선택한 키 중 하나라도 존재하지 않으면 아웃바운드 메시지가 \"실패\"를 보고합니다.", + "created-time": "생성 일시", + "chip-help": "'Enter'를 눌러 {{inputName}} 입력을 완료하세요.\n'Backspace'를 눌러 {{inputName}}을(를) 삭제하세요.\n여러 값이 지원됩니다.", + "detail": "상세", + "field-name": "필드 이름", + "device-profile": "장치 프로필", + "entity-type": "엔티티 유형", + "message-type": "메시지 유형", + "timeseries-key": "시계열 키", + "type": "유형", + "first-name": "이름", + "last-name": "성", + "label": "라벨", + "originator-fields-mapping": "발신자 필드 매핑", + "add-mapped-originator-fields-to": "매핑된 발신자 필드 추가 위치", + "fields": "필드", + "skip-empty-fields": "빈 필드 건너뛰기", + "skip-empty-fields-tooltip": "빈 값이 있는 필드는 출력 메시지/출력 메타데이터에 추가되지 않습니다.", + "fetch-interval": "가져오기 간격", + "fetch-strategy": "가져오기 전략", + "fetch-timeseries-from-to": "{{startInterval}} {{startIntervalTimeUnit}} 전부터 {{endInterval}} {{endIntervalTimeUnit}} 전까지의 시계열을 가져옵니다.", + "fetch-timeseries-from-to-invalid": "시계열 가져오기가 잘못되었습니다 (\"간격 시작\"은 \"간격 종료\"보다 작아야 함).", + "use-metadata-dynamic-interval-tooltip": "선택하면 규칙 노드가 메시지와 메타데이터 패턴을 기반으로 동적 간격 시작 및 종료를 사용합니다.", + "all-mode-hint": "가져오기 모드 \"모두\"를 선택하면 규칙 노드가 구성 가능한 쿼리 매개변수로 가져오기 간격에서 텔레메트리를 검색합니다.", + "first-mode-hint": "가져오기 모드 \"첫 번째\"를 선택하면 규칙 노드가 가져오기 간격의 시작에 가장 가까운 텔레메트리를 검색합니다.", + "last-mode-hint": "가져오기 모드 \"마지막\"을 선택하면 규칙 노드가 가져오기 간격의 종료에 가장 가까운 텔레메트리를 검색합니다.", + "ascending": "오름차순", + "descending": "내림차순", + "min": "최소", + "max": "최대", + "average": "평균", + "sum": "합계", + "count": "숫자", + "none": "없음", + "last-level-relation-tooltip": "선택하면 규칙 노드가 최대 관계 레벨에 설정된 레벨에서만 관련 엔티티를 검색합니다.", + "last-level-device-relation-tooltip": "선택하면 규칙 노드가 최대 관계 레벨에 설정된 레벨에서만 관련 장치를 검색합니다.", + "data-to-fetch": "가져올 데이터", + "mapping-of-customers": "고객 매핑", + "map-fields-required": "모든 매핑 필드가 필요합니다.", + "attributes": "속성", + "related-device-attributes": "관련 장치 속성", + "add-selected-attributes-to": "선택한 속성 추가 위치", + "device-profiles": "장치 프로필", + "mapping-of-tenant": "테넌트 매핑", + "add-attribute-key": "속성 키 추가", + "message-template": "메시지 템플릿", + "message-template-required": "메시지 템플릿이 필요합니다", + "use-system-slack-settings": "시스템 Slack 설정 사용", + "slack-api-token": "Slack API 토큰", + "slack-api-token-required": "Slack API 토큰이 필요합니다", + "keys-mapping": "키 매핑", + "add-key": "키 추가", + "recipients": "수신자", + "message-subject-and-content": "메시지 제목 및 내용", + "template-rules-hint": "두 입력 필드 모두 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메시지 메타데이터에서 값을 추출하세요.", + "originator-customer-desc": "수신 메시지 발신자의 고객을 새 발신자로 사용합니다.", + "originator-tenant-desc": "현재 테넌트를 새 발신자로 사용합니다.", + "originator-related-entity-desc": "관련 엔티티를 새 발신자로 사용합니다. 구성된 관계 유형과 방향을 기반으로 조회합니다.", + "originator-alarm-originator-desc": "알람 발신자를 새 발신자로 사용합니다. 수신 메시지 발신자가 알람 엔티티인 경우에만.", + "originator-entity-by-name-pattern-desc": "DB에서 가져온 엔티티를 새 발신자로 사용합니다. 엔티티 유형과 지정된 이름 패턴을 기반으로 조회합니다.", + "email-from-template-hint": "$[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "recipients-block-main-hint": "쉼표로 구분된 주소 목록입니다. 모든 입력 필드는 템플릿을 지원합니다. $[messageKey]를 사용하여 메시지에서 값을 추출하고 ${metadataKey}를 사용하여 메타데이터에서 값을 추출하세요.", + "forward-msg-default-rule-chain": "메시지를 발신자의 기본 규칙 체인으로 전달", + "forward-msg-default-rule-chain-tooltip": "활성화하면 메시지가 발신자의 기본 규칙 체인으로 전달되거나, 발신자가 엔티티 프로필에 기본 규칙 체인을 정의하지 않은 경우 구성의 규칙 체인으로 전달됩니다.", + "exclude-zero-deltas": "아웃바운드 메시지에서 0 델타 제외", + "exclude-zero-deltas-hint": "활성화하면 값이 0이 아닌 경우 \"{{outputValueKey}}\" 출력 키가 아웃바운드 메시지에 추가됩니다.", + "exclude-zero-deltas-time-difference-hint": "활성화하면 \"{{outputValueKey}}\" 값이 0이 아닌 경우에만 \"{{outputValueKey}}\" 및 \"{{periodValueKey}}\" 출력 키가 아웃바운드 메시지에 추가됩니다.", + "search-direction-from": "발신자에서 대상 엔티티로", + "search-direction-to": "대상 엔티티에서 발신자로", + "del-relation-direction-from": "발신자에서", + "del-relation-direction-to": "발신자로", + "target-entity": "대상 엔티티", + "function-configuration": "함수 구성", + "function-name": "함수 이름", + "function-name-required": "함수 이름이 필요합니다.", + "qualifier": "한정자", + "qualifier-hint": "한정자가 지정되지 않으면 기본 한정자 \"$LATEST\"가 사용됩니다.", + "aws-credentials": "AWS 자격 증명", + "connection-timeout": "연결 시간 제한", + "connection-timeout-required": "연결 시간 초과는 필수 항목입니다.", + "connection-timeout-min": "최소 연결 시간 제한은 0입니다.", + "connection-timeout-hint": "연결을 처음 설정할 때 포기하고 시간 초과하기 전까지 기다리는 시간(초)입니다. 값이 0이면 무한을 의미하며 권장되지 않습니다.", + "request-timeout": "요청 시간 초과", + "request-timeout-required": "요청 시간 제한이 필요합니다", + "request-timeout-min": "최소 요청 시간 초과는 0입니다.", + "request-timeout-hint": "요청이 완료될 때까지 포기하고 시간 초과하기 전까지 기다리는 시간(초)입니다. 값이 0이면 무한을 의미하며 권장되지 않습니다.", + "units": "단위", + "tell-failure-aws-lambda": "AWS Lambda 함수 실행에서 예외가 발생하면 실패 알림", + "tell-failure-aws-lambda-hint": "AWS Lambda 함수 실행이 예외를 발생시키면 규칙 노드가 메시지 처리 실패를 강제합니다.", + "basic-mode": "기본", + "advanced-mode": "고급", + "save-time-series": { + "processing-settings": "처리 설정", + "processing-settings-hint": "수신 메시지가 처리되는 방식을 정의합니다. 기본 처리 설정에서는 미리 구성된 전략을 선택할 수 있고, 고급 설정에서는 각 작업에 대한 개별 처리 전략을 선택할 수 있습니다.", + "advanced-settings-hint": "처리 전략을 구성할 때 주의하세요. 특정 조합은 예기치 않은 동작을 초래할 수 있습니다.", + "strategy": "전략", + "deduplication-interval": "중복 제거 간격", + "deduplication-interval-required": "중복 제거 간격은 필수 항목입니다.", + "deduplication-interval-min-max-range": "중복 제거 간격은 최소 1초에서 최대 1일이어야 합니다.", + "strategy-type": { + "every-message": "모든 메시지마다", + "skip": "건너뛰기", + "deduplicate": "중복 제거", + "web-sockets-only": "WebSocket만" + }, + "time-series": "시계열", + "latest": "최근 값", + "web-sockets": "웹소켓", + "calculated-fields-and-alarm-rules": "계산 필드 및 알람 규칙" + }, + "save-attribute": { + "processing-settings": "처리 설정", + "processing-settings-hint": "수신 메시지가 처리되는 방식을 정의합니다. 기본 처리 설정에서는 미리 구성된 전략을 선택할 수 있고, 고급 설정에서는 각 작업에 대한 개별 처리 전략을 선택할 수 있습니다.", + "advanced-settings-hint": "처리 전략을 구성할 때 주의하세요. 특정 조합은 예기치 않은 동작을 초래할 수 있습니다.", + "strategy": "전략", + "deduplication-interval": "중복 제거 간격", + "deduplication-interval-required": "중복 제거 간격은 필수 항목입니다.", + "deduplication-interval-min-max-range": "중복 제거 간격은 최소 1초에서 최대 1일이어야 합니다.", + "scope": "범위", + "strategy-type": { + "every-message": "모든 메시지마다", + "skip": "건너뛰기", + "deduplicate": "중복 제거", + "web-sockets-only": "WebSocket만" + }, + "attributes": "속성" + }, + "key-val": { + "key": "키", + "value": "값", + "see-examples": "예제를 참조하세요.", + "remove-entry": "앤트리 삭제", + "remove-mapping-entry": "매핑 항목 제거", + "add-mapping-entry": "매핑 추가", + "add-entry": "앤트리 추가", + "copy-key-values-from": "키-값 복사 원본", + "delete-key-values": "키-값 삭제", + "delete-key-values-from": "키-값 삭제 원본", + "at-least-one-key-error": "최소 하나의 키를 선택해야 합니다.", + "unique-key-value-pair-error": "'{{keyText}}'은(는) '{{valText}}'와(과) 달라야 합니다!" + }, + "mail-body-types": { + "plain-text": "일반 텍스트", + "html": "HTML", + "dynamic": "동적", + "use-body-type-template": "본문 유형 템플릿 사용", + "plain-text-description": "특별한 스타일이나 형식이 없는 간단한 서식 없는 텍스트입니다.", + "html-text-description": "메일 본문에 서식, 링크 및 이미지를 위한 HTML 태그를 사용할 수 있습니다.", + "dynamic-text-description": "템플릿 기능을 기반으로 일반 텍스트 또는 HTML 본문 유형을 동적으로 사용할 수 있습니다.", + "after-template-evaluation-hint": "템플릿 평가 후 값은 HTML의 경우 true, 일반 텍스트의 경우 false여야 합니다." + }, + "ai": { + "ai-model": "AI 모델", + "model": "모델", + "ai-model-hint": "이 규칙 노드에서 보낸 요청을 처리할 미리 구성된 AI 모델을 선택하거나 \"새로 만들기\"를 사용하여 새 모델을 구성하세요.", + "prompt-settings": "프롬프트 설정", + "prompt-settings-hint": "선택적 시스템 프롬프트는 AI의 일반적인 역할과 제약 조건을 설정하고, 사용자 프롬프트는 수행할 특정 작업을 정의합니다. 두 필드 모두 템플릿을 지원합니다.", + "system-prompt": "시스템 프롬프트", + "system-prompt-max-length": "시스템 프롬프트는 500000자 이하여야 합니다.", + "system-prompt-blank": "시스템 프롬프트는 비워둘 수 없습니다.", + "user-prompt": "사용자 프롬프트", + "user-prompt-required": "사용자 프롬프트는 필수 항목입니다.", + "user-prompt-max-length": "사용자 프롬프트는 500000자 이하여야 합니다.", + "user-prompt-blank": "사용자 프롬프트는 비워둘 수 없습니다.", + "response-format": "응답 형식", + "response-text": "텍스트", + "response-json": "JSON", + "response-json-schema": "JSON 스키마", + "response-format-hint-TEXT": "모델이 임의의 텍스트를 생성할 수 있습니다. 이는 유효한 JSON 엔티티이거나 아닐 수 있습니다. 출력이 유효한 JSON 엔티티가 아니면 \"response\" 키 아래의 JSON 엔티티 내에 자동으로 래핑됩니다.", + "response-format-hint-JSON": "모델은 유효한 JSON인 응답을 생성해야 합니다. 출력이 유효한 JSON 엔티티가 아니면 \"response\" 키 아래의 JSON 엔티티 내에 자동으로 래핑됩니다.", + "response-format-hint-JSON_SCHEMA": "모델은 제공된 스키마에 정의된 특정 구조와 데이터 유형과 일치하는 JSON을 생성해야 합니다. 출력이 유효한 JSON 엔티티가 아니면 \"response\" 키 아래의 JSON 엔티티 내에 자동으로 래핑됩니다.", + "response-json-schema-hint": "유효한 JSON 스키마를 입력할 수 있지만 이 규칙 노드는 기능의 제한된 하위 집합만 지원합니다. 자세한 내용은 노드 문서를 참조하세요.", + "response-json-schema-required": "JSON 스키마는 필수 항목입니다.", + "advanced-settings": "고급 설정", + "timeout": "시간 초과", + "timeout-hint": "요청이 종료되기 전에 AI 모델로부터 응답을 기다리는 최대 시간.", + "timeout-required": "시간 초과는 필수 항목입니다.", + "timeout-validation": "1초에서 10분 사이여야 합니다.", + "force-acknowledgement": "강제 확인", + "force-acknowledgement-hint": "활성화하면 수신 메시지가 즉시 확인됩니다. 그런 다음 모델의 응답이 별도의 새 메시지로 큐에 추가됩니다.", + "ai-resources": "AI 리소스" + } }, "timezone": { - "timezone": "Timezone", - "select-timezone": "Select timezone", - "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", - "timezone-required": "Timezone is required." + "timezone": "시간대", + "select-timezone": "시간대 선택", + "no-timezones-matching": "'{{timezone}}'와(과) 일치하는 시간대를 찾을 수 없습니다.", + "timezone-required": "시간대는 필수 항목입니다.", + "browser-time": "브라우저 시간" }, "queue": { - "select_name": "Select queue name", - "name": "Queue Name", - "name_required": "Queue name is required!" + "queue-name": "큐", + "no-queues-found": "큐를 찾을 수 없습니다.", + "no-queues-matching": "'{{queue}}'와(과) 일치하는 큐를 찾을 수 없습니다.", + "select-name": "큐 이름 선택", + "name": "이름", + "name-required": "큐 이름은 필수 항목입니다!", + "name-unique": "큐 이름이 고유하지 않습니다!", + "name-pattern": "큐 이름에 ASCII 영숫자, '.', '_', '-' 이외의 문자가 포함되어 있습니다!", + "queue-required": "큐는 필수 항목입니다!", + "topic-required": "큐 토픽은 필수 항목입니다!", + "poll-interval-required": "폴링 간격은 필수 항목입니다!", + "poll-interval-min-value": "폴링 간격 값은 1 미만일 수 없습니다.", + "partitions-required": "파티션은 필수 항목입니다!", + "partitions-min-value": "파티션 값은 1 미만일 수 없습니다.", + "pack-processing-timeout-required": "처리 시간 초과는 필수 항목입니다.", + "pack-processing-timeout-min-value": "처리 시간 초과 값은 1 미만일 수 없습니다.", + "batch-size-required": "배치 크기는 필수 항목입니다!", + "batch-size-min-value": "배치 크기 값은 1 미만일 수 없습니다.", + "retries-required": "재시도는 필수 항목입니다!", + "retries-min-value": "재시도 값은 음수일 수 없습니다.", + "failure-percentage-required": "실패 비율은 필수 항목입니다!", + "failure-percentage-min-value": "실패 비율 값은 0 미만일 수 없습니다.", + "failure-percentage-max-value": "실패 비율 값은 100을 초과할 수 없습니다.", + "pause-between-retries-required": "재시도 사이의 일시 중지는 필수 항목입니다!", + "pause-between-retries-min-value": "재시도 사이의 일시 중지 값은 1 미만일 수 없습니다.", + "max-pause-between-retries-required": "재시도 사이의 최대 일시 중지는 필수 항목입니다!", + "max-pause-between-retries-min-value": "재시도 사이의 최대 일시 중지 값은 1 미만일 수 없습니다.", + "submit-strategy-type-required": "제출 전략 유형은 필수 항목입니다!", + "processing-strategy-type-required": "처리 전략 유형은 필수 항목입니다!", + "queues": "큐", + "selected-queues": "{ count, plural, =1 {1개 큐} other {#개 큐} } 선택됨", + "delete-queue-title": "큐 '{{queueName}}'을(를) 삭제하시겠습니까?", + "delete-queues-title": "{ count, plural, =1 {1개 큐} other {#개 큐} }를 삭제하시겠습니까?", + "delete-queue-text": "주의: 확인 후 큐와 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-queues-text": "확인 후 선택한 모든 큐가 삭제되어 액세스할 수 없게 됩니다.", + "search": "큐 검색", + "add": "큐 추가", + "details": "큐 세부 정보", + "topic": "토픽", + "submit-settings": "제출 설정", + "submit-strategy": "전략 유형 *", + "grouping-parameter": "그룹화 매개변수", + "processing-settings": "재시도 처리 설정", + "processing-strategy": "처리 유형 *", + "retries-settings": "재시도 설정", + "polling-settings": "폴링 설정", + "batch-processing": "배치 처리", + "poll-interval": "폴링 간격", + "partitions": "파티션", + "immediate-processing": "즉시 처리", + "consumer-per-partition": "각 소비자에게 메시지 폴 전송", + "consumer-per-partition-hint": "각 파티션에 별도의 소비자 활성화", + "duplicate-msg-to-all-partitions": "모든 파티션에 메시지 복제", + "processing-timeout": "처리 시간 내 (ms)", + "batch-size": "배치 크기", + "retries": "재시도 횟수 (0 – 무제한)", + "failure-percentage": "재시도 건너뛰기 실패 메시지 (%)", + "pause-between-retries": "재시도 시간 내 (초)", + "max-pause-between-retries": "추가 재시도 시간 내 (초)", + "delete": "큐 삭제", + "copyId": "큐 ID 복사", + "idCopiedMessage": "큐 ID가 클립보드에 복사되었습니다.", + "description": "설명", + "description-hint": "이 텍스트는 선택한 전략 대신 큐 설명에 표시됩니다", + "alt-description": "제출 전략: {{submitStrategy}}, 처리 전략: {{processingStrategy}}", + "custom-properties": "사용자 지정 속성", + "custom-properties-hint": "사용자 지정 큐 (토픽) 생성 속성, 예: 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies": { + "sequential-by-originator-label": "발신자별 순차", + "sequential-by-originator-hint": "예: 장치 A의 이전 메시지가 확인될 때까지 장치 A의 새 메시지가 제출되지 않습니다", + "sequential-by-tenant-label": "테넌트별 순차", + "sequential-by-tenant-hint": "예: 테넌트 A의 이전 메시지가 확인될 때까지 테넌트 A의 새 메시지가 제출되지 않습니다", + "sequential-label": "순차", + "sequential-hint": "새 메시지는 이전 메시지가 확인될 때까지 제출되지 않습니다.", + "burst-label": "버스트", + "burst-hint": "모든 메시지가 도착한 순서대로 규칙 체인에 제출됩니다.", + "batch-label": "배치", + "batch-hint": "새 배치는 이전 배치가 확인될 때까지 제출되지 않습니다.", + "skip-all-failures-label": "모든 실패 건너뛰기", + "skip-all-failures-hint": "모든 실패 무시", + "skip-all-failures-and-timeouts-label": "모든 실패 및 시간 초과 건너뛰기", + "skip-all-failures-and-timeouts-hint": "모든 실패 및 시간 초과 무시", + "retry-all-label": "모두 재시도", + "retry-all-hint": "처리 팩의 모든 메시지 재시도", + "retry-failed-label": "실패 재시도", + "retry-failed-hint": "처리 팩의 모든 실패한 메시지 재시도", + "retry-timeout-label": "시간 초과 재시도", + "retry-timeout-hint": "처리 팩의 모든 시간 초과된 메시지 재시도", + "retry-failed-and-timeout-label": "실패 및 시간 초과 재시도", + "retry-failed-and-timeout-hint": "처리 팩의 모든 실패 및 시간 초과된 메시지 재시도" + } + }, + "queue-statistics": { + "queue-statistics": "큐 통계", + "no-queue-statistics-matching": "'{{entity}}'와(과) 일치하는 큐 통계를 찾을 수 없습니다.", + "queue-statistics-required": "큐 통계는 필수 항목입니다.", + "list-of-queue-statistics": "{ count, plural, =1 {하나의 큐 통계} other {#개 큐 통계 목록} }", + "selected-queue-statistics": "{ count, plural, =1 {1개 큐 통계} other {#개 큐 통계} } 선택됨", + "no-queue-statistics-text": "큐 통계를 찾을 수 없습니다", + "queue-statistics-starts-with": "이름이 '{{prefix}}'로 시작하는 큐 통계" + }, + "server-error": { + "general": "일반 서버 오류", + "authentication": "인증 오류", + "jwt-token-expired": "JWT 토큰 만료", + "tenant-trial-expired": "테넌트 평가판 만료", + "credentials-expired": "자격 증명 만료", + "permission-denied": "권한 거부", + "invalid-arguments": "잘못된 인수", + "bad-request-params": "잘못된 요청 매개변수", + "item-not-found": "항목을 찾을 수 없음", + "too-many-requests": "요청이 너무 많음", + "too-many-updates": "업데이트가 너무 많음", + "entities-limit-exceeded": "엔티티 제한 초과" }, "tenant": { "tenant": "테넌트", @@ -1900,6 +6176,7 @@ "add-tenant-text": "테넌트 추가", "no-tenants-text": "테넌트가 없습니다.", "tenant-details": "테넌트 상세 정보", + "title-max-length": "제목은 256자 미만이어야 합니다.", "delete-tenant-title": "'{{tenantTitle}}' 테넌트를 삭제하시겠습니까?", "delete-tenant-text": "테넌트 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "delete-tenants-title": "{ count, plural, =1 {테넌트 1 개} other {테넌트 # 개} }를 삭제하시겠습니까?", @@ -1917,91 +6194,277 @@ "tenant-required": "테넌트가 필요합니다.", "search": "테넌트 검색", "selected-tenants": "{ count, plural, =1 {1 개 테넌트} other {# 개 테넌트} } 선택됨", - "isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container", - "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant" - }, + "isolated-tb-rule-engine": "격리된 ThingsBoard 규칙 엔진 큐 사용", + "isolated-tb-rule-engine-details": "각 테넌트는 전용 규칙 엔진 큐를 갖습니다" + }, "tenant-profile": { - "tenant-profile": "테넌트 프로파일", - "tenant-profiles": "테넌트 프로파일", - "add": "테넌트 프로파일 추가", - "edit": "테넌트 프로파일 편집", - "tenant-profile-details": "Tenant profile details", - "no-tenant-profiles-text": "아무 테넌트 프로파일도 찾지 못했습니다", - "search": "테넌트 프로파일 검색", - "selected-tenant-profiles": "{ count, plural, =1 {1 개 테넌트 프로파일} other {# 개 테넌트 프로파일} } 선택됨", - "no-tenant-profiles-matching": "No tenant profile matching '{{entity}}' were found.", - "tenant-profile-required": "테넌트 프로파일을 입력하세요", - "idCopiedMessage": "테넌트 프로파일 ID가 클립보드에 저장되었습니다", - "set-default": "테넌트 프로파일 기본값으로 지정", - "delete": "테넌트 프로파일 삭제", - "copyId": "테넌트 프로파일 ID 복사", + "tenant-profile": "테넌트 프로필", + "tenant-profiles": "테넌트 프로필", + "add": "테넌트 프로필 추가", + "add-profile": "프로필 추가", + "debug": "디버그", + "edit": "테넌트 프로필 편집", + "tenant-profile-details": "테넌트 프로필 세부 정보", + "no-tenant-profiles-text": "아무 테넌트 프로필도 찾지 못했습니다", + "name-max-length": "이름은 256자 미만이어야 합니다.", + "search": "테넌트 프로필 검색", + "selected-tenant-profiles": "{ count, plural, =1 {1 개 테넌트 프로필} other {# 개 테넌트 프로필} } 선택됨", + "no-tenant-profiles-matching": "'{{entity}}'와(과) 일치하는 테넌트 프로필을 찾을 수 없습니다.", + "tenant-profile-required": "테넌트 프로필을 입력하세요", + "idCopiedMessage": "테넌트 프로필 ID가 클립보드에 저장되었습니다", + "set-default": "테넌트 프로필 기본값으로 지정", + "delete": "테넌트 프로필 삭제", + "copyId": "테넌트 프로필 ID 복사", "name": "이름", "name-required": "이름을 입력하세요.", - "data": "프로파일 데이터", - "profile-configuration": "프로파일 설정", + "data": "프로필 데이터", + "profile-configuration": "프로필 설정", "description": "설명", "default": "기본값", - "delete-tenant-profile-title": "Are you sure you want to delete the tenant profile '{{tenantProfileName}}'?", - "delete-tenant-profile-text": "Be careful, after the confirmation the tenant profile and all related data will become unrecoverable.", - "delete-tenant-profiles-title": "{ count, plural, =1 {1 개 테넌트 프로파일} other {# 개 테넌트 프로파일} }을 삭제하시겠습니까?", - "delete-tenant-profiles-text": "Be careful, after the confirmation all selected tenant profiles will be removed and all related data will become unrecoverable.", - "set-default-tenant-profile-title": "Are you sure you want to make the tenant profile '{{tenantProfileName}}' default?", - "set-default-tenant-profile-text": "After the confirmation the tenant profile will be marked as default and will be used for new tenants with no profile specified.", - "no-tenant-profiles-found": "아무 테넌트 프로파일도 찾지 못했습니다.", - "create-new-tenant-profile": "Create a new one!", + "delete-tenant-profile-title": "테넌트 프로필 '{{tenantProfileName}}'을(를) 삭제하시겠습니까?", + "delete-tenant-profile-text": "주의: 확인 후 테넌트 프로필과 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-tenant-profiles-title": "{ count, plural, =1 {1 개 테넌트 프로필} other {# 개 테넌트 프로필} }을 삭제하시겠습니까?", + "delete-tenant-profiles-text": "주의: 확인 후 선택한 모든 테넌트 프로필이 제거되고 모든 관련 데이터를 복구할 수 없게 됩니다.", + "set-default-tenant-profile-title": "테넌트 프로필 '{{tenantProfileName}}'을(를) 기본값으로 설정하시겠습니까?", + "set-default-tenant-profile-text": "확인 후 테넌트 프로필이 기본값으로 표시되며 프로필이 지정되지 않은 새 테넌트에 사용됩니다.", + "no-tenant-profiles-found": "아무 테넌트 프로필도 찾지 못했습니다.", + "create-new-tenant-profile": "새로 만들기!", + "create-tenant-profile": "새 테넌트 프로필 생성", + "import": "테넌트 프로필 가져오기", + "export": "테넌트 프로필 내보내기", + "export-failed-error": "테넌트 프로필을 내보낼 수 없습니다: {{error}}", + "tenant-profile-file": "테넌트 프로필 파일", + "invalid-tenant-profile-file-error": "테넌트 프로필을 가져올 수 없습니다: 잘못된 테넌트 프로필 데이터 구조입니다.", + "advanced-settings": "고급 설정", + "entities": "엔티티", + "rule-engine": "규칙 엔진", + "time-to-live": "수명", + "calculated-fields": "계산 필드", + "alarms-and-notifications": "알람 및 알림", + "ota-files-in-bytes": "파일", + "ws-title": "WS", + "unlimited": "(0 - 무제한)", "maximum-devices": "최대 장치 수 (0 - 무제한)", - "maximum-devices-required": "Maximum number of devices is required.", - "maximum-devices-range": "Minimum number of devices can't be negative", + "maximum-devices-required": "장치 최대 수는 필수 항목입니다.", + "maximum-devices-range": "장치 최대 수는 음수일 수 없습니다.", "maximum-assets": "최대 자산 수 (0 - 무제한)", - "maximum-assets-required": "Maximum number of assets is required.", - "maximum-assets-range": "Maximum number of assets can't be negative", - "maximum-customers": "최대 커스터머 수 (0 - 무제한)", - "maximum-customers-required": "최대 커스터머 수를 입력하세요.", - "maximum-customers-range": "Maximum number of customers can't be negative", - "maximum-users": "Maximum number of users (0 - unlimited)", - "maximum-users-required": "Maximum number of users is required.", - "maximum-users-range": "Maximum number of users can't be negative", - "maximum-dashboards": "Maximum number of dashboards (0 - unlimited)", - "maximum-dashboards-required": "Maximum number of dashboards is required.", - "maximum-dashboards-range": "Maximum number of dashboards can't be negative", - "maximum-edges": "Edges maximum number (0 - unlimited)", - "maximum-edges-required": "Edges maximum number is required.", - "maximum-edges-range": "Edges maximum number can't be negative", - "maximum-rule-chains": "Maximum number of rule chains (0 - unlimited)", - "maximum-rule-chains-required": "Maximum number of rule chains is required.", - "maximum-rule-chains-range": "Maximum number of rule chains can't be negative", - "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages rate limit.", - "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points rate limit.", - "transport-device-msg-rate-limit": "Transport device messages rate limit.", - "transport-device-telemetry-msg-rate-limit": "Transport device telemetry messages rate limit.", - "transport-device-telemetry-data-points-rate-limit": "Transport device telemetry data points rate limit.", - "max-transport-messages": "Maximum number of transport messages (0 - unlimited)", - "max-transport-messages-required": "Maximum number of transport messages is required.", - "max-transport-messages-range": "Maximum number of transport messages can't be negative", - "max-transport-data-points": "Maximum number of transport data points (0 - unlimited)", - "max-transport-data-points-required": "Maximum number of transport data points is required.", - "max-transport-data-points-range": "Maximum number of transport data points can't be negative", - "max-r-e-executions": "Maximum number of Rule Engine executions (0 - unlimited)", - "max-r-e-executions-required": "Maximum number of Rule Engine executions is required.", - "max-r-e-executions-range": "Maximum number of Rule Engine executions can't be negative", - "max-j-s-executions": "Maximum number of JavaScript executions (0 - unlimited)", - "max-j-s-executions-required": "Maximum number of JavaScript executions is required.", - "max-j-s-executions-range": "Maximum number of JavaScript executions can't be negative", - "max-d-p-storage-days": "Maximum number of data points storage days (0 - unlimited)", - "max-d-p-storage-days-required": "Maximum number of data points storage days is required.", - "max-d-p-storage-days-range": "Maximum number of data points storage days can't be negative", - "default-storage-ttl-days": "Default storage TTL days (0 - unlimited)", - "default-storage-ttl-days-required": "Default storage TTL days is required.", - "default-storage-ttl-days-range": "Default storage TTL days can't be negative", - "max-rule-node-executions-per-message": "Maximum number of rule node executions per message (0 - unlimited)", - "max-rule-node-executions-per-message-required": "Maximum number of rule node executions per message is required.", - "max-rule-node-executions-per-message-range": "Maximum number of rule node executions per message can't be negative", - "max-emails": "Maximum number of emails sent (0 - unlimited)", - "max-emails-required": "Maximum number of emails sent is required.", - "max-emails-range": "Maximum number of emails sent can't be negative", - "max-sms": "Maximum number of SMS sent (0 - unlimited)", - "max-sms-required": "Maximum number of SMS sent is required.", - "max-sms-range": "Maximum number of SMS sent can't be negative" + "maximum-assets-required": "자산 최대 수는 필수 항목입니다.", + "maximum-assets-range": "자산 최대 수는 음수일 수 없습니다.", + "maximum-customers": "최대 고객 수 (0 - 무제한)", + "maximum-customers-required": "최대 고객 수를 입력하세요.", + "maximum-customers-range": "고객 최대 수는 음수일 수 없습니다.", + "maximum-users": "사용자 최대 수", + "maximum-users-required": "사용자 최대 수는 필수 항목입니다.", + "maximum-users-range": "사용자 최대 수는 음수일 수 없습니다.", + "maximum-dashboards": "대시보드 최대 수", + "maximum-dashboards-required": "대시보드 최대 수는 필수 항목입니다.", + "maximum-dashboards-range": "대시보드 최대 수는 음수일 수 없습니다.", + "maximum-edges": "엣지 최대 수", + "maximum-edges-required": "Edge 최대 수는 필수 항목입니다.", + "maximum-edges-range": "Edge 최대 수는 음수일 수 없습니다.", + "maximum-rule-chains": "규칙 체인 최대 수", + "maximum-rule-chains-required": "규칙 체인 최대 수는 필수 항목입니다.", + "maximum-rule-chains-range": "규칙 체인 최대 수는 음수일 수 없습니다.", + "maximum-resources-sum-data-size": "리소스 파일의 최대 총 크기 (바이트)", + "maximum-resources-sum-data-size-required": "리소스 파일의 최대 총 크기는 필수 항목입니다.", + "maximum-resources-sum-data-size-range": "리소스 파일의 최대 총 크기는 음수일 수 없습니다.", + "maximum-resource-size": "최대 리소스 파일 크기 (바이트)", + "maximum-resource-size-required": "최대 리소스 파일 크기는 필수 항목입니다.", + "maximum-resource-size-range": "최대 리소스 파일 크기는 음수일 수 없습니다.", + "maximum-ota-packages-sum-data-size": "OTA 패키지 파일의 최대 총 크기 (바이트)", + "maximum-ota-package-sum-data-size-required": "OTA 패키지 파일의 최대 총 크기는 필수 항목입니다.", + "maximum-ota-package-sum-data-size-range": "OTA 패키지 파일의 최대 총 크기는 음수일 수 없습니다.", + "maximum-debug-duration-min": "최대 디버그 기간 (분)", + "maximum-debug-duration-min-range": "최대 디버그 기간은 음수일 수 없습니다.", + "rest-requests-for-tenant": "테넌트에 대한 REST 요청", + "transport-tenant-telemetry-msg-rate-limit": "테넌트 텔레메트리 메시지 전송", + "transport-tenant-telemetry-data-points-rate-limit": "테넌트 텔레메트리 데이터 포인트 전송", + "transport-device-msg-rate-limit": "장치 메시지 전송", + "transport-device-telemetry-msg-rate-limit": "장치 텔레메트리 메시지 전송", + "transport-device-telemetry-data-points-rate-limit": "장치 텔레메트리 데이터 포인트 전송", + "transport-gateway-msg-rate-limit": "전송 게이트웨이 메시지", + "transport-gateway-telemetry-msg-rate-limit": "전송 게이트웨이 텔레메트리 메시지", + "transport-gateway-telemetry-data-points-rate-limit": "전송 게이트웨이 텔레메트리 데이터 포인트", + "transport-gateway-device-msg-rate-limit": "전송 게이트웨이 장치 메시지", + "transport-gateway-device-telemetry-msg-rate-limit": "전송 게이트웨이 장치 텔레메트리 메시지", + "transport-gateway-device-telemetry-data-points-rate-limit": "전송 게이트웨이 장치 텔레메트리 데이터 포인트", + "tenant-entity-export-rate-limit": "엔티티 버전 생성", + "tenant-entity-import-rate-limit": "엔티티 버전 불러오기", + "tenant-notification-request-rate-limit": "알림 요청", + "tenant-notification-requests-per-rule-rate-limit": "알림 규칙당 알림 요청", + "max-calculated-fields": "엔티티당 계산 필드 최대 수", + "max-calculated-fields-range": "엔티티당 계산 필드 최대 수는 음수일 수 없습니다", + "max-calculated-fields-required": "엔티티당 계산 필드 최대 수가 필요합니다", + "max-data-points-per-rolling-arg": "롤링 인수의 최대 데이터 포인트 수", + "max-data-points-per-rolling-arg-range": "롤링 인수의 최대 데이터 포인트 수는 음수일 수 없습니다", + "max-data-points-per-rolling-arg-required": "롤링 인수의 최대 데이터 포인트 수가 필요합니다", + "max-arguments-per-cf": "계산 필드당 인수 최대 수", + "max-arguments-per-cf-range": "계산 필드당 인수 최대 수는 음수일 수 없습니다", + "max-arguments-per-cf-required": "계산 필드당 인수 최대 수가 필요합니다", + "max-related-level-per-argument": "'관련 엔티티' 인수당 최대 관계 레벨", + "max-related-level-per-argument-range": "'관련 엔티티' 인수당 관계 레벨 최대 수는 '1'보다 작을 수 없습니다", + "max-related-level-per-argument-required": "'관련 엔티티' 인수당 관계 레벨 최대 수가 필요합니다", + "min-allowed-scheduled-update-interval": "'관련 엔티티' 인수에 허용되는 최소 업데이트 간격 (초)", + "min-allowed-scheduled-update-interval-range": "허용되는 최소 업데이트 간격 최소 수는 음수일 수 없습니다", + "min-allowed-deduplication-interval": "허용되는 최소 중복 제거 간격 (초)", + "min-allowed-deduplication-interval-range": "허용되는 최소 중복 제거 간격 값은 음수일 수 없습니다", + "min-allowed-deduplication-interval-required": "허용되는 최소 중복 제거 간격이 필요합니다", + "intermediate-aggregation-interval": "중간 집계 간격 (초)", + "intermediate-aggregation-interval-range": "중간 집계 간격 값은 '1'보다 작을 수 없습니다", + "intermediate-aggregation-interval-required": "중간 집계 간격이 필요합니다", + "reevaluation-check-interval": "재평가 확인 간격 (초)", + "reevaluation-check-interval-range": "재평가 확인 간격 값은 '1'보다 작을 수 없습니다", + "reevaluation-check-interval-required": "재평가 확인 간격이 필요합니다", + "alarms-reevaluation-interval": "알람 재평가 간격 (초)", + "alarms-reevaluation-interval-range": "알람 재평가 간격 값은 '1'보다 작을 수 없습니다", + "alarms-reevaluation-interval-required": "알람 재평가 간격이 필요합니다", + "min-allowed-aggregation-interval": "허용되는 최소 집계 간격 (초)", + "min-allowed-aggregation-interval-range": "허용되는 최소 집계 간격 값은 음수일 수 없습니다", + "min-allowed-aggregation-interval-required": "허용되는 최소 집계 간격이 필요합니다", + "min-allowed-scheduled-update-interval-required": "허용되는 최소 업데이트 간격 최소 수가 필요합니다", + "max-state-size": "상태 최대 크기 (KB)", + "max-state-size-range": "상태 최대 크기 (KB)는 음수일 수 없습니다", + "max-state-size-required": "상태 최대 크기 (KB)가 필요합니다", + "max-value-argument-size": "단일 값 인수 최대 크기 (KB)", + "max-value-argument-size-range": "단일 값 인수 최대 크기 (KB)는 음수일 수 없습니다", + "max-value-argument-size-required": "단일 값 인수 최대 크기 (KB)가 필요합니다", + "max-transport-messages": "전송 메시지 최대 수", + "max-transport-messages-required": "전송 메시지 최대 수는 필수 항목입니다.", + "max-transport-messages-range": "전송 메시지 최대 수는 음수일 수 없습니다.", + "max-transport-data-points": "전송 데이터 포인트 최대 수", + "max-transport-data-points-required": "전송 데이터 포인트 최대 수는 필수 항목입니다.", + "max-transport-data-points-range": "전송 데이터 포인트 최대 수는 음수일 수 없습니다.", + "max-r-e-executions": "규칙 엔진 실행 최대 수", + "max-r-e-executions-required": "규칙 엔진 실행 최대 수는 필수 항목입니다.", + "max-r-e-executions-range": "규칙 엔진 실행 최대 수는 음수일 수 없습니다.", + "max-j-s-executions": "JavaScript 실행 최대 수", + "max-j-s-executions-required": "JavaScript 실행 최대 수는 필수 항목입니다.", + "max-j-s-executions-range": "JavaScript 실행 최대 수는 음수일 수 없습니다.", + "max-tbel-executions": "TBEL 실행 최대 수", + "max-tbel-executions-required": "TBEL 실행 최대 수가 필요합니다.", + "max-tbel-executions-range": "TBEL 실행 최대 수는 음수일 수 없습니다", + "max-d-p-storage-days": "데이터 포인트 저장 일수 최대 수", + "max-d-p-storage-days-required": "데이터 포인트 저장 일수 최대 수는 필수 항목입니다.", + "max-d-p-storage-days-range": "데이터 포인트 저장 일수 최대 수는 음수일 수 없습니다.", + "default-storage-ttl-days": "기본 저장소 TTL 일수", + "default-storage-ttl-days-required": "기본 저장소 TTL 일수는 필수 항목입니다.", + "default-storage-ttl-days-range": "기본 저장소 TTL 일수는 음수일 수 없습니다.", + "alarms-ttl-days": "알람 TTL 일수", + "alarms-ttl-days-required": "알람 TTL 일수가 필요합니다", + "alarms-ttl-days-days-range": "알람 TTL 일수는 음수일 수 없습니다", + "rpc-ttl-days": "RPC TTL 일수", + "rpc-ttl-days-required": "RPC TTL 일수가 필요합니다", + "rpc-ttl-days-days-range": "RPC TTL 일수는 음수일 수 없습니다", + "queue-stats-ttl-days": "큐 통계 TTL 일수", + "queue-stats-ttl-days-required": "큐 통계 TTL 일수가 필요합니다", + "queue-stats-ttl-days-range": "큐 통계 TTL 일수는 음수일 수 없습니다", + "rule-engine-exceptions-ttl-days": "규칙 엔진 예외 TTL 일수", + "rule-engine-exceptions-ttl-days-required": "규칙 엔진 예외 TTL 일수가 필요합니다", + "rule-engine-exceptions-ttl-days-range": "규칙 엔진 예외 TTL 일수는 음수일 수 없습니다", + "max-rule-node-executions-per-message": "메시지당 규칙 노드 실행 최대 수", + "max-rule-node-executions-per-message-required": "메시지당 규칙 노드 실행 최대 수는 필수 항목입니다.", + "max-rule-node-executions-per-message-range": "메시지당 규칙 노드 실행 최대 수는 음수일 수 없습니다.", + "max-emails": "발송 이메일 최대 수", + "max-emails-required": "발송 이메일 최대 수는 필수 항목입니다.", + "max-emails-range": "발송 이메일 최대 수는 음수일 수 없습니다.", + "sms-enabled": "SMS 활성화됨", + "max-sms": "발송 SMS 최대 수", + "max-sms-required": "발송 SMS 최대 수는 필수 항목입니다.", + "max-sms-range": "발송 SMS 최대 수는 음수일 수 없습니다.", + "max-created-alarms": "생성된 알람 최대 수", + "max-created-alarms-required": "생성된 알람 최대 수가 필요합니다.", + "max-created-alarms-range": "생성된 알람 최대 수는 음수일 수 있습니다", + "no-queue": "구성된 큐 없음", + "add-queue": "큐 추가", + "queues-with-count": "큐 ({{count}})", + "tenant-rest-limits": "테넌트에 대한 REST 요청", + "customer-rest-limits": "고객에 대한 REST 요청", + "incorrect-pattern-for-rate-limits": "형식은 용량과 기간(초)의 쉼표로 구분된 쌍이며 콜론으로 구분됩니다. 예: 100:1,2000:60", + "too-small-value-zero": "값은 0보다 커야 합니다", + "too-small-value-one": "값은 1보다 커야 합니다", + "queue-size-is-limited-by-system-configuration": "큐 크기는 시스템 구성에 의해서도 제한됩니다.", + "cassandra-write-tenant-core-limits-configuration": "REST API Cassandra 쓰기 쿼리", + "cassandra-read-tenant-core-limits-configuration": "REST API 및 WS 텔레메트리 Cassandra 읽기 쿼리", + "cassandra-write-tenant-rule-engine-limits-configuration": "규칙 엔진 텔레메트리 Cassandra 쓰기 쿼리", + "cassandra-read-tenant-rule-engine-limits-configuration": "규칙 엔진 텔레메트리 Cassandra 읽기 쿼리", + "ws-limit-max-sessions-per-tenant": "테넌트당 최대 세션 수", + "ws-limit-max-sessions-per-customer": "고객당 최대 세션 수", + "ws-limit-max-sessions-per-regular-user": "일반 사용자당 최대 세션 수", + "ws-limit-max-sessions-per-public-user": "공개 사용자당 최대 세션 수", + "ws-limit-queue-per-session": "세션당 최대 메시지 큐 크기", + "ws-limit-max-subscriptions-per-tenant": "테넌트당 최대 구독 수", + "ws-limit-max-subscriptions-per-customer": "고객당 최대 구독 수", + "ws-limit-max-subscriptions-per-regular-user": "일반 사용자당 최대 구독 수", + "ws-limit-max-subscriptions-per-public-user": "공개 사용자당 최대 구독 수", + "ws-limit-updates-per-session": "세션당 WS 업데이트", + "relation-search-entity-limit": "관계 검색 엔티티 제한", + "relation-search-entity-limit-hint": "관계 경로의 마지막 레벨에서 확인되는 엔티티 수를 제한합니다. '관련 엔티티' 인수 및 전파 필드에 적용됩니다.", + "relation-search-entity-limit-required": "관계 검색 엔티티 제한", + "relation-search-entity-limit-range": "관계 검색 엔티티 제한은 '1'보다 작을 수 없습니다", + "rate-limits": { + "add-limit": "제한 추가", + "and-also-less-than": "그리고 다음보다 작음", + "advanced-settings": "고급 설정", + "edit-limit": "제한 편집", + "calculated-field-debug-event-rate-limit": "계산 필드 디버그 이벤트", + "edit-calculated-field-debug-event-rate-limit": "계산 필드 디버그 이벤트 속도 제한 편집", + "edit-transport-tenant-msg-title": "전송 테넌트 메시지 속도 제한 편집", + "edit-transport-tenant-telemetry-msg-title": "전송 테넌트 텔레메트리 메시지 속도 제한 편집", + "edit-transport-tenant-telemetry-data-points-title": "전송 테넌트 텔레메트리 데이터 포인트 속도 제한 편집", + "edit-transport-device-msg-title": "전송 장치 메시지 속도 제한 편집", + "edit-transport-device-telemetry-msg-title": "전송 장치 텔레메트리 메시지 속도 제한 편집", + "edit-transport-device-telemetry-data-points-title": "전송 장치 텔레메트리 데이터 포인트 속도 제한 편집", + "edit-transport-gateway-msg-title": "전송 게이트웨이 메시지 속도 제한 편집", + "edit-transport-gateway-telemetry-msg-title": "전송 게이트웨이 텔레메트리 메시지 속도 제한 편집", + "edit-transport-gateway-telemetry-data-points-title": "전송 게이트웨이 텔레메트리 데이터 포인트 속도 제한 편집", + "edit-transport-gateway-device-msg-title": "전송 게이트웨이 장치 메시지 속도 제한 편집", + "edit-transport-gateway-device-telemetry-msg-title": "전송 게이트웨이 장치 텔레메트리 메시지 속도 제한 편집", + "edit-transport-gateway-device-telemetry-data-points-title": "전송 게이트웨이 장치 텔레메트리 데이터 포인트 속도 제한 편집", + "edit-tenant-rest-limits-title": "테넌트에 대한 REST 요청 속도 제한 편집", + "edit-customer-rest-limits-title": "고객에 대한 REST 요청 속도 제한 편집", + "edit-ws-limit-updates-per-session-title": "세션당 WS 업데이트 속도 제한 편집", + "edit-cassandra-write-tenant-core-limits-configuration": "Rest API Cassandra 쓰기 쿼리 편집", + "edit-cassandra-read-tenant-core-limits-configuration": "Rest API 및 WS 텔레메트리 Cassandra 읽기 쿼리 편집", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "규칙 엔진 텔레메트리 Cassandra 쓰기 쿼리 편집", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "규칙 엔진 텔레메트리 Cassandra 읽기 쿼리 편집", + "edit-tenant-entity-export-rate-limit-title": "엔티티 버전 생성 속도 제한 편집", + "edit-tenant-entity-import-rate-limit-title": "엔티티 버전 로드 속도 제한 편집", + "edit-tenant-notification-request-rate-limit-title": "알림 요청 속도 제한 편집", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "알림 규칙당 알림 요청 속도 제한 편집", + "edit-edge-events-rate-limit": "엣지 이벤트 속도 제한 편집", + "edit-edge-events-per-edge-rate-limit": "엣지당 엣지 이벤트 속도 제한 편집", + "edge-events-rate-limit": "엣지 이벤트", + "edge-events-per-edge-rate-limit": "엣지당 엣지 이벤트", + "edit-edge-uplink-messages-rate-limit": "엣지 업링크 메시지 속도 제한 편집", + "edit-edge-uplink-messages-per-edge-rate-limit": "엣지당 엣지 업링크 메시지 속도 제한 편집", + "edge-uplink-messages-rate-limit": "엣지 업링크 메시지", + "edge-uplink-messages-per-edge-rate-limit": "엣지당 업링크 메시지", + "messages-per": "메시지당", + "not-set": "설정되지 않음", + "number-of-messages": "메시지 수", + "number-of-messages-required": "메시지 수가 필요합니다.", + "number-of-messages-min": "최소 값은 1입니다.", + "preview": "미리보기", + "per-seconds": "초당", + "per-seconds-required": "시간 속도가 필요합니다.", + "per-seconds-min": "최소 값은 1입니다.", + "per-seconds-duplicate": "중복 시간 속도입니다. 각 시간 간격은 고유해야 합니다.", + "rate-limits": "속도 제한", + "remove-limit": "제한 제거", + "transport-tenant-msg": "전송 테넌트 메시지", + "transport-tenant-telemetry-msg": "테넌트 텔레메트리 메시지 전송", + "transport-tenant-telemetry-data-points": "테넌트 텔레메트리 데이터 포인트 전송", + "transport-device-msg": "장치 메시지 전송", + "transport-device-telemetry-msg": "장치 텔레메트리 메시지 전송", + "transport-device-telemetry-data-points": "장치 텔레메트리 데이터 포인트 전송", + "transport-gateway-msg": "전송 게이트웨이 메시지", + "transport-gateway-telemetry-msg": "전송 게이트웨이 텔레메트리 메시지", + "transport-gateway-telemetry-data-points": "전송 게이트웨이 텔레메트리 데이터 포인트", + "transport-gateway-device-msg": "전송 게이트웨이 장치 메시지", + "transport-gateway-device-telemetry-msg": "전송 게이트웨이 장치 텔레메트리 메시지", + "transport-gateway-device-telemetry-data-points": "전송 게이트웨이 장치 텔레메트리 데이터 포인트", + "sec": "초" + } }, "timeinterval": { "seconds-interval": "{ seconds, plural, =1 {1 초} other {# 초} }", @@ -2012,37 +6475,691 @@ "hours": "시간", "minutes": "분", "seconds": "초", - "advanced": "고급" + "advanced": "고급", + "custom": "사용자 지정", + "predefined": { + "yesterday": "어제", + "day-before-yesterday": "그저께", + "this-day-last-week": "지난주 오늘", + "previous-week": "지난주 (일 - 토)", + "previous-week-iso": "지난주 (월 - 일)", + "previous-month": "지난달", + "previous-quarter": "지난 분기", + "previous-half-year": "지난 반기", + "previous-year": "작년", + "current-hour": "현재 시간", + "current-day": "오늘", + "current-day-so-far": "오늘 현재", + "current-week": "이번주 (일 - 토)", + "current-week-iso": "이번주 (월 - 일)", + "current-week-so-far": "이번주 현재 (일 - 토)", + "current-week-iso-so-far": "이번주 현재 (월 - 일)", + "current-month": "이번달", + "current-month-so-far": "이번달 현재", + "current-quarter": "이번 분기", + "current-quarter-so-far": "이번 분기 현재", + "current-half-year": "이번 반기", + "current-half-year-so-far": "이번 반기 현재", + "current-year": "올해", + "current-year-so-far": "올해 현재" + }, + "type": { + "week": "주 (일 - 토)", + "week-iso": "주 (월 - 일)", + "month": "달", + "quarter": "분기" + } }, "timeunit": { + "milliseconds": "밀리초", "seconds": "초", "minutes": "분", "hours": "시간", "days": "일" }, "timewindow": { + "timewindow": "타임윈도우", + "timewindow-settings": "시간 창 설정", + "years": "{ years, plural, =1 {1년 } other {#년 } }", + "years-short": "{{ years }}년", + "months": "{ months, plural, =1 {1개월 } other {#개월 } }", + "months-short": "{{ months }}개월", + "weeks": "{ weeks, plural, =1 {1주 } other {#주 } }", + "weeks-short": "{{ weeks }}주", "days": "{ days, plural, =1 {1 일 } other {# 일 } }", + "days-short": "{{ days }}일", "hours": "{ hours, plural, =0 {0 시간 } =1 {1 시간 } other {# 시간 } }", + "hr": "{{ hr }} 시간", + "hr-short": "{{ hr }}시간", "minutes": "{ minutes, plural, =0 {0 분 } =1 {1 분 } other {# 분 } }", + "min": "{{ min }} 분", + "min-short": "{{ min }}분", "seconds": "{ seconds, plural, =0 {0 초 } =1 {1 초 } other {# 초 } }", + "sec": "{{ sec }} 초", + "sec-short": "{{ sec }}초", + "short": { + "years": "{ years, plural, =1 {1년 } other {#년 } }", + "days": "{ days, plural, =1 {1 일 } other {# 일 } }", + "hours": "{ hours, plural, =1 {1시간 } other {#시간 } }", + "minutes": "{{minutes}}분 ", + "seconds": "{{seconds}}초 " + }, "realtime": "실시간", "history": "기록", "last-prefix": "과거", "period": "{{ startTime }}부터 {{ endTime }}까지", "edit": "타임윈도우 편집", "date-range": "날짜 범위", + "for-all-time": "전체 기간", "last": "과거", "time-period": "기간", - "hide": "숨기기" + "hide": "숨기기", + "interval": "간격", + "just-now": "방금", + "just-now-lower": "방금", + "ago": "전", + "style": "시간 창 스타일", + "icon": "아이콘", + "icon-position": "아이콘 위치", + "icon-position-left": "왼쪽", + "icon-position-right": "오른쪽", + "font": "글꼴", + "color": "색상", + "displayTypePrefix": "실시간/기록 접두사 표시", + "preview": "미리보기", + "relative": "상대적", + "range": "범위", + "hide-timewindow-section": "최종 사용자에게 시간 창 섹션 숨기기", + "hide-last-interval": "최종 사용자에게 마지막 간격 숨기기", + "hide-relative-interval": "최종 사용자에게 상대적 간격 숨기기", + "hide-fixed-interval": "최종 사용자에게 고정 간격 숨기기", + "hide-aggregation": "최종 사용자에게 집계 숨기기", + "hide-group-interval": "최종 사용자에게 그룹화 간격 숨기기", + "hide-max-values": "최종 사용자에게 최대 값 숨기기", + "hide-timezone": "최종 사용자에게 시간대 숨기기", + "disable-custom-interval": "사용자 지정 간격 선택 비활성화", + "edit-aggregation-functions-list": "집계 함수 목록 편집", + "edit-aggregation-functions-list-hint": "사용 가능한 옵션 목록을 지정할 수 있습니다.", + "allowed-aggregation-functions": "허용된 집계 함수", + "edit-intervals-list": "간격 목록 편집", + "allowed-agg-intervals": "그룹화 간격", + "default-agg-interval": "기본 그룹화 간격", + "edit-intervals-list-hint": "사용 가능한 간격 옵션 목록을 지정할 수 있습니다.", + "edit-grouping-intervals-list-hint": "그룹화 간격 목록과 기본 그룹화 간격을 구성할 수 있습니다.", + "all": "모두", + "save-current-settings-as-default": "현재 설정을 기본 시간 창으로 저장", + "hide-option-from-end-users": "최종 사용자에게 옵션 숨기기" + }, + "tooltip": { + "trigger": "트리거", + "trigger-point": "지점", + "trigger-axis": "축", + "label": "라벨", + "value": "값", + "date": "날짜", + "show-date-time-interval": "날짜 시간 간격 표시", + "show-date-time-interval-hint": "데이터 집계에 따라 날짜 시간 간격을 표시합니다.", + "hide-zero-tooltip-values": "0 값 숨기기", + "show-stack-total": "스택 모드에서 총 값 표시", + "background-color": "배경색", + "background-blur": "배경 블러" + }, + "unit": { + "set-unit-conversion": "단위 변환 설정", + "unit-settings": { + "unit-settings": "단위 설정", + "source-unit": "소스 단위", + "source-unit-hint": "저장된 값의 단위입니다. 변환할 원본 단위입니다. 소스 데이터에서 사용하는 기호를 입력하세요 (예: m, km, ft, in).", + "target-metric-unit": "대상 미터법 단위", + "target-metric-unit-hint": "소스 값을 변환할 미터법 단위(SI)를 선택하세요 (예: cm, mm, km).", + "target-imperial-unit": "대상 야드파운드법 단위", + "target-imperial-unit-hint": "소스 값을 변환할 야드파운드법 단위를 선택하세요 (예: in, ft, yd).", + "target-hybrid-unit": "대상 하이브리드 단위", + "target-hybrid-unit-hint": "소스 값을 변환할 하이브리드 단위를 선택하세요 (예: cm, in, km). 하이브리드 단위는 미터법 또는 야드파운드법 단위를 결합합니다.", + "enable-unit-conversion": "단위 변환 활성화", + "enable-unit-conversion-hint": "켜기로 전환하여 변환을 활성화합니다. 꺼져 있으면 소스 값이 변경되지 않고 전달됩니다. 해당 측정 그룹에 단위가 하나만 있는 경우 비활성화됩니다 (예: 광속, AQI)." + }, + "unit-system": "단위 시스템", + "unit-system-type": { + "AUTO": "자동", + "METRIC": "미터법", + "IMPERIAL": "야드파운드법", + "HYBRID": "하이브리드" + }, + "measures": { + "absorbed-dose-rate": "흡수 선량률", + "acceleration": "가속도", + "acidity": "산도", + "air-quality-index": "대기질 지수", + "amount-of-substance": "물질량", + "angle": "각도", + "angular-acceleration": "각가속도", + "area": "영역", + "area-density": "면적 밀도", + "capacitance": "정전 용량", + "catalytic-activity": "촉매 활성", + "catalytic-concentration": "촉매 농도", + "charge": "전하", + "current-density": "전류 밀도", + "data-transfer-rate": "데이터 전송 속도", + "density": "밀도", + "digital": "디지털", + "dimension-ratio": "차원 비율", + "dynamic-viscosity": "동점도", + "earthquake-magnitude": "지진 규모", + "electric-charge-density": "전하 밀도", + "electric-current": "전류", + "electric-dipole-moment": "전기 쌍극자 모멘트", + "electric-field-strength": "전기장 세기", + "electric-flux": "전기 선속", + "electric-permittivity": "유전율", + "electric-polarizability": "전기 분극률", + "electrical-conductance": "전도도", + "electrical-conductivity": "전도율", + "energy": "에너지", + "energy-density": "에너지 밀도", + "force": "힘", + "frequency": "주파수", + "fuel-efficiency": "연료 효율", + "heat-capacity": "열용량", + "illuminance": "조도", + "inductance": "인덕턴스", + "kinematic-viscosity": "동점도", + "length": "길이", + "light-exposure": "노광량", + "linear-charge-density": "선형 전하 밀도", + "logarithmic-ratio": "로그 비율", + "luminous-efficacy": "발광 효율", + "luminous-flux": "광선속", + "luminous-intensity": "광도", + "magnetic-field-gradient": "자기장 기울기", + "magnetic-flux": "자기 선속", + "magnetic-flux-density": "자기 선속 밀도", + "magnetic-moment": "자기 모멘트", + "magnetic-permeability": "투자율", + "mass": "질량", + "mass-fraction": "질량 분율", + "molar-concentration": "몰 농도", + "molar-energy": "몰 에너지", + "molar-heat-capacity": "몰 열용량", + "molar-mass": "몰 질량", + "number-concentration": "수 농도", + "parts-per-million": "백만분율", + "power": "전력", + "power-density": "전력 밀도", + "pressure": "압력", + "radiance": "복사 휘도", + "radiant-intensity": "복사 강도", + "radiation-dose": "방사선량", + "radioactive-decay": "방사성 붕괴", + "radioactivity": "방사능", + "radioactivity-concentration": "방사능 농도", + "reciprocal-length": "역 길이", + "resistance": "저항", + "reynolds-number": "레이놀즈 수", + "signal-level": "신호 레벨", + "solid-angle": "입체각", + "specific-energy": "비에너지", + "specific-heat-capacity": "비열", + "specific-humidity": "비습도", + "specific-volume": "비체적", + "speed": "속도", + "surface-charge-density": "표면 전하 밀도", + "surface-tension": "표면 장력", + "temperature": "온도", + "thermal-conductivity": "열전도율", + "time": "시간", + "torque": "토크", + "turbidity": "탁도", + "voltage": "전압", + "volume": "부피", + "volume-flow": "체적 유량" + }, + "millimeter": "밀리미터", + "centimeter": "센티미터", + "decimeter": "데시미터", + "angstrom": "옹스트롬", + "nanometer": "나노미터", + "micrometer": "마이크로미터", + "meter": "미터", + "kilometer": "킬로미터", + "inch": "인치", + "foot": "피트", + "foot-us": "피트 (미국 측량)", + "yard": "야드", + "mile": "마일", + "nautical-mile": "해리", + "astronomical-unit": "천문 단위", + "reciprocal-metre": "역 미터", + "meter-per-meter": "미터당 미터", + "steradian": "스테라디안", + "thou": "파운드", + "barleycorn": "보리콘", + "hand": "핸드", + "chain": "체인", + "furlong": "퍼롱", + "league": "리그", + "fathom": "패덤", + "cable": "케이블", + "link": "링크", + "rod": "로드", + "nanogram": "나노그램", + "microgram": "마이크로그램", + "milligram": "밀리그램", + "gram": "그램", + "kilogram": "킬로그램", + "tonne": "톤", + "ounce": "온스", + "pound": "파운드", + "stone": "스톤", + "hundredweight-count": "헌드레드웨이트", + "short-tons": "단톤", + "dalton": "달튼", + "grain": "그레인", + "drachm": "드램", + "quarter": "분기", + "slug": "슬러그", + "carat": "캐럿", + "cubic-millimeter": "세제곱밀리미터", + "cubic-centimeter": "세제곱센티미터", + "cubic-meter": "세제곱미터", + "cubic-kilometer": "세제곱킬로미터", + "microliter": "마이크로리터", + "milliliter": "밀리리터", + "liter": "리터", + "hectoliter": "헥토리터", + "cubic-inch": "세제곱인치", + "cubic-foot": "세제곱피트", + "cubic-yard": "세제곱야드", + "fluid-ounce": "액량 온스", + "fluid-ounce-per-second": "초당 액량 온스", + "pint": "파인트", + "quart": "쿼트", + "gallon": "갤런", + "oil-barrels": "석유 배럴", + "cubic-meter-per-kilogram": "킬로그램당 세제곱미터", + "gill": "길", + "hogshead": "혹스헤드", + "teaspoon": "티스푼", + "tablespoon": "테이블스푼", + "cup": "컵", + "celsius": "섭씨", + "kelvin": "켈빈", + "rankine": "랭킨", + "fahrenheit": "화씨", + "percent": "퍼센트", + "meter-per-second": "미터 매 초", + "kilometer-per-hour": "킬로미터 매 시간", + "foot-per-second": "피트 매 초", + "foot-per-minute": "피트 매 분", + "mile-per-hour": "마일 매 시간", + "knot": "노트", + "inch-per-second": "인치 매 초", + "inch-per-hour": "인치 매 시간", + "millimeters-per-minute": "밀리미터 매 분", + "meter-per-minute": "미터 매 분", + "kilometer-per-hour-squared": "킬로미터 매 시간 제곱", + "foot-per-second-squared": "피트 매 초 제곱", + "pascal": "파스칼", + "kilopascal": "킬로파스칼", + "megapascal": "메가파스칼", + "gigapascal": "기가파스칼", + "millibar": "밀리바", + "bar": "막대", + "kilobar": "킬로바", + "newton": "뉴턴", + "newton-meter": "뉴턴 미터", + "foot-pounds": "피트파운드", + "inch-pounds": "인치파운드", + "newton-per-meter": "뉴턴 매 미터", + "atmospheres": "기압", + "pounds-per-square-inch": "파운드 매 제곱인치", + "kilopound-per-square-inch": "킬로파운드 매 제곱인치", + "torr": "토르", + "inches-of-mercury": "수은주 인치", + "pascal-per-square-meter": "파스칼 매 제곱미터", + "pound-per-square-inch": "파운드 매 제곱인치", + "newton-per-square-meter": "뉴턴 매 제곱미터", + "kilogram-force-per-square-meter": "킬로그램힘 매 제곱미터", + "pascal-per-square-centimeter": "파스칼 매 제곱센티미터", + "ton-force-per-square-inch": "톤힘 매 제곱인치", + "kilonewton-per-square-meter": "킬로뉴턴 매 제곱미터", + "newton-per-square-millimeter": "뉴턴 매 제곱밀리미터", + "microjoule": "마이크로줄", + "millijoule": "밀리줄", + "joule": "줄", + "kilojoule": "킬로줄", + "megajoule": "메가줄", + "gigajoule": "기가줄", + "watt-hour": "와트시", + "watt-minute": "와트분", + "kilowatt-hour": "킬로와트시", + "milliwatt-hour": "밀리와트시", + "megawatt-hour": "메가와트시", + "gigawatt-hour": "기가와트시", + "electron-volts": "전자볼트", + "joules-per-coulomb": "줄 매 쿨롱", + "british-thermal-unit": "영국 열량 단위", + "thousand-british-thermal-unit": "천 영국 열량 단위", + "million-british-thermal-unit": "백만 영국 열량 단위", + "foot-pound": "피트파운드", + "calorie": "칼로리", + "small-calorie": "소칼로리", + "kilocalorie": "킬로칼로리", + "joule-per-kelvin": "줄 매 켈빈", + "joule-per-kilogram-kelvin": "줄 매 킬로그램켈빈", + "joule-per-kilogram": "줄 매 킬로그램", + "watt-per-meter-kelvin": "와트 매 미터켈빈", + "joule-per-cubic-meter": "줄 매 세제곱미터", + "therm": "덤", + "electric-dipole-moment": "전기 쌍극자 모멘트", + "magnetic-dipole-moment": "자기 쌍극자 모멘트", + "debye": "데바이", + "coulomb-per-square-meter-per-volt": "쿨롱 매 제곱미터 매 볼트", + "milliwatt": "밀리와트", + "microwatt": "마이크로와트", + "watt": "와트", + "kilowatt": "킬로와트", + "megawatt": "메가와트", + "gigawatt": "기가와트", + "metric-horsepower": "미터법 마력", + "milliwatt-per-square-centimeter": "밀리와트 매 제곱센티미터", + "watt-per-square-centimeter": "와트 매 제곱센티미터", + "kilowatt-per-square-centimeter": "킬로와트 매 제곱센티미터", + "milliwatt-per-square-meter": "밀리와트 매 제곱미터", + "watt-per-square-meter": "와트 매 제곱미터", + "kilowatt-per-square-meter": "킬로와트 매 제곱미터", + "watt-per-square-inch": "와트 매 제곱인치", + "kilowatt-per-square-inch": "킬로와트 매 제곱인치", + "horsepower": "마력", + "btu-per-hour": "영국 열량 단위 매 시간", + "btu-per-second": "영국 열량 단위 매 초", + "btu-per-day": "영국 열량 단위 매 일", + "mbtu-per-hour": "천 영국 열량 단위 매 시간", + "mbtu-per-second": "천 영국 열량 단위 매 초", + "mbtu-per-day": "천 영국 열량 단위 매 일", + "mmbtu-per-hour": "백만 영국 열량 단위 매 시간", + "mmbtu-per-second": "백만 영국 열량 단위 매 초", + "mmbtu-per-day": "백만 영국 열량 단위 매 일", + "foot-pound-per-second": "피트파운드 매 초", + "coulomb": "쿨롱", + "millicoulomb": "밀리쿨롱", + "microcoulomb": "마이크로쿨롱", + "nanocoulomb": "나노쿨롱", + "picocoulomb": "피코쿨롱", + "coulomb-per-meter": "쿨롱 매 미터", + "coulomb-per-cubic-meter": "쿨롱 매 세제곱미터", + "coulomb-per-square-meter": "쿨롱 매 제곱미터", + "square-millimeter": "제곱밀리미터", + "square-centimeter": "제곱센티미터", + "square-meter": "제곱미터", + "hectare": "헥타르", + "square-kilometer": "제곱킬로미터", + "square-inch": "제곱인치", + "square-foot": "제곱피트", + "square-yard": "제곱야드", + "acre": "에이커", + "square-mile": "제곱마일", + "are": "아르", + "barn": "반", + "circular-inch": "원형 인치", + "milliampere-hour": "밀리암페어시", + "ampere-hours": "암페어시", + "kiloampere-hours": "킬로암페어시", + "nanoampere": "나노암페어", + "picoampere": "피코암페어", + "microampere": "마이크로암페어", + "milliampere": "밀리암페어", + "ampere": "암페어", + "kiloampere": "킬로암페어", + "megaampere": "메가암페어", + "gigaampere": "기가암페어", + "microampere-per-square-centimeter": "마이크로암페어 매 제곱센티미터", + "ampere-per-square-meter": "암페어 매 제곱미터", + "ampere-per-meter": "암페어 매 미터", + "oersted": "에르스테드", + "bohr-magneton": "보어 마그네톤", + "ampere-meter-squared": "암페어미터 제곱", + "nanovolt": "나노볼트", + "picovolt": "피코볼트", + "millivolt": "밀리볼트", + "microvolt": "마이크로볼트", + "volt": "볼트", + "kilovolt": "킬로볼트", + "megavolt": "메가볼트", + "dbmV": "데시벨 볼트", + "dbm": "데시벨 밀리와트", + "volt-meter": "볼트미터", + "kilovolt-meter": "킬로볼트미터", + "megavolt-meter": "메가볼트미터", + "microvolt-meter": "마이크로볼트미터", + "millivolt-meter": "밀리볼트미터", + "nanovolt-meter": "나노볼트미터", + "ohm": "옴", + "microohm": "마이크로옴", + "milliohm": "밀리옴", + "kilohm": "킬로옴", + "megohm": "메가옴", + "gigohm": "기가옴", + "millihertz": "밀리헤르츠", + "hertz": "헤르츠", + "kilohertz": "킬로헤르츠", + "megahertz": "메가헤르츠", + "gigahertz": "기가헤르츠", + "terahertz": "테라헤르츠", + "rpm": "분당 회전수", + "candela-per-square-meter": "칸델라 매 제곱미터", + "candela": "칸델라", + "lumen": "루멘", + "lux": "럭스", + "foot-candle": "피트캔들", + "lumen-per-square-meter": "루멘 매 제곱미터", + "lux-second": "럭스 초", + "lumen-second": "루멘 초", + "lumens-per-watt": "루멘 매 와트", + "mole": "몰", + "nanomole": "나노몰", + "micromole": "마이크로몰", + "millimole": "밀리몰", + "kilomole": "킬로몰", + "mole-per-cubic-meter": "몰 매 세제곱미터", + "rssi": "수신 신호 강도 표시기", + "ppm": "백만분율", + "ppb": "10억분의 1", + "micrograms-per-cubic-meter": "마이크로그램 매 세제곱미터", + "aqi": "AQI", + "gram-per-cubic-meter": "그램 매 세제곱미터", + "gram-per-kilogram": "비습도", + "millimeters-per-second": "밀리미터 매 초", + "neper": "네퍼", + "bel": "벨", + "decibel": "데시벨", + "meters-per-second-squared": "미터 매 초 제곱", + "becquerel": "베크렐", + "curie": "퀴리", + "gray": "그레이", + "sievert": "시버트", + "roentgen": "뢴트겐", + "cps": "초당 카운트", + "rad": "라드", + "rem": "렘", + "dps": "초당 붕괴", + "rutherford": "러더퍼드", + "coulombs-per-kilogram": "쿨롱 매 킬로그램", + "becquerels-per-cubic-meter": "베크렐 매 세제곱미터", + "curies-per-liter": "퀴리 매 리터", + "becquerels-per-second": "초당 베크렐", + "curies-per-second": "초당 퀴리", + "gy-per-second": "초당 그레이", + "watt-per-steradian": "스테라디안당 와트", + "watt-per-square-metre-steradian": "제곱미터-스테라디안당 와트", + "ph-level": "pH 레벨", + "turbidity": "탁도", + "mg-per-liter": "리터당 밀리그램", + "microsiemens-per-centimeter": "센티미터당 마이크로지멘스", + "millisiemens-per-meter": "미터당 밀리지멘스", + "siemens-per-meter": "미터당 지멘스", + "kilogram-per-cubic-meter": "세제곱미터당 킬로그램", + "gram-per-cubic-centimeter": "세제곱센티미터당 그램", + "kilogram-per-square-meter": "제곱미터당 킬로그램", + "milligram-per-milliliter": "밀리리터당 밀리그램", + "milligram-per-cubic-meter": "세제곱미터당 밀리그램", + "pound-per-cubic-foot": "세제곱피트당 파운드", + "ounces-per-cubic-inch": "세제곱인치당 온스", + "tons-per-cubic-yard": "세제곱야드당 톤", + "particle-density": "입자 밀도", + "kilometers-per-liter": "리터당 킬로미터", + "miles-per-gallon": "갤런당 마일", + "liters-per-100-km": "100km당 리터", + "gallons-per-mile": "마일당 갤런", + "liters-per-hour": "시간당 리터", + "gallons-per-hour": "시간당 갤런", + "beats-per-minute": "분당 박동수", + "millimeters-of-mercury": "수은주 밀리미터", + "milligrams-per-deciliter": "데시리터당 밀리그램", + "g-force": "중력 가속도", + "kilonewton": "킬로뉴턴", + "kilogram-force": "킬로그램힘", + "pound-force": "파운드힘", + "kilopound-force": "킬로파운드힘", + "dyne": "다인", + "poundal": "파운달", + "kip": "킵", + "gal": "갈", + "gravity": "중력", + "hectopascal": "헥토파스칼", + "atmosphere": "기압", + "millibars": "밀리바", + "inch-of-mercury": "수은주 인치", + "richter-scale": "리히터 규모", + "nanosecond": "나노초", + "microsecond": "마이크로초", + "millisecond": "밀리초", + "second": "초", + "minute": "분", + "hour": "시간", + "day": "일", + "week": "주", + "month": "달", + "year": "년", + "cubic-foot-per-minute": "분당 세제곱피트", + "cubic-meters-per-hour": "시간당 세제곱미터", + "cubic-meters-per-second": "초당 세제곱미터", + "liter-per-second": "초당 리터", + "liter-per-minute": "분당 리터", + "gallons-per-minute": "분당 갤런", + "cubic-foot-per-second": "초당 세제곱피트", + "milliliters-per-minute": "분당 밀리리터", + "cubic-decimeter-per-second": "초당 세제곱데시미터", + "bit": "비트", + "byte": "바이트", + "kilobyte": "킬로바이트", + "megabyte": "메가바이트", + "gigabyte": "기가바이트", + "terabyte": "테라바이트", + "petabyte": "페타바이트", + "exabyte": "엑사바이트", + "zettabyte": "제타바이트", + "yottabyte": "요타바이트", + "bit-per-second": "초당 비트", + "kilobit-per-second": "초당 킬로비트", + "megabit-per-second": "초당 메가비트", + "gigabit-per-second": "초당 기가비트", + "terabit-per-second": "초당 테라비트", + "byte-per-second": "초당 바이트", + "kilobyte-per-second": "초당 킬로바이트", + "megabyte-per-second": "초당 메가바이트", + "gigabyte-per-second": "초당 기가바이트", + "degree": "도", + "radian": "라디안", + "gradian": "그라디안", + "arcminute": "각분", + "arcsecond": "각초", + "milliradian": "밀리라디안", + "revolution": "회전", + "siemens": "지멘스", + "millisiemens": "밀리지멘스", + "microsiemens": "마이크로지멘스", + "kilosiemens": "킬로지멘스", + "megasiemens": "메가지멘스", + "gigasiemens": "기가지멘스", + "farad": "패럿", + "millifarad": "밀리패럿", + "microfarad": "마이크로패럿", + "nanofarad": "나노패럿", + "picofarad": "피코패럿", + "kilofarad": "킬로패럿", + "megafarad": "메가패럿", + "gigafarad": "기가패럿", + "terfarad": "테라패럿", + "farad-per-meter": "미터당 패럿", + "tesla": "테슬라", + "gauss": "가우스", + "kilogauss": "킬로가우스", + "millitesla": "밀리테슬라", + "microtesla": "마이크로테슬라", + "nanotesla": "나노테슬라", + "kilotesla": "킬로테슬라", + "megatesla": "메가테슬라", + "millitesla-square-meters": "밀리테슬라 제곱미터", + "gamma": "감마", + "lambda": "람다", + "square-meter-per-second": "초당 제곱미터", + "square-centimeter-per-second": "초당 제곱센티미터", + "stoke": "스토크", + "centistokes": "센티스토크", + "square-foot-per-second": "초당 제곱피트", + "square-inch-per-second": "초당 제곱인치", + "pascal-second": "파스칼-초", + "centipoise": "센티푸아즈", + "poise": "푸아즈", + "reynolds": "레이놀즈", + "pound-per-foot-hour": "피트-시간당 파운드", + "newton-second-per-square-meter": "제곱미터당 뉴턴 초", + "dyne-second-per-square-centimeter": "제곱센티미터당 다인 초", + "kilogram-per-meter-second": "미터-초당 킬로그램", + "tesla-square-meters": "테슬라 제곱미터", + "maxwell": "맥스웰", + "tesla-per-meter": "미터당 테슬라", + "gauss-per-centimeter": "센티미터당 가우스", + "weber": "웨버", + "microweber": "마이크로웨버", + "milliweber": "밀리웨버", + "gauss-square-centimeter": "가우스-제곱센티미터", + "kilogauss-square-centimeter": "킬로가우스-제곱센티미터", + "henry": "헨리", + "millihenry": "밀리헨리", + "microhenry": "마이크로헨리", + "nanohenry": "나노헨리", + "henry-per-meter": "미터당 헨리", + "tesla-meter-per-ampere": "암페어당 테슬라 미터", + "gauss-per-oersted": "에르스테드당 가우스", + "kilogram-per-mole": "몰당 킬로그램", + "gram-per-mole": "몰당 그램", + "milligram-per-mole": "몰당 밀리그램", + "joule-per-mole": "몰당 줄", + "joule-per-mole-kelvin": "몰-켈빈당 줄", + "millivolts-per-meter": "미터당 밀리볼트", + "volts-per-meter": "미터당 볼트", + "kilovolts-per-meter": "미터당 킬로볼트", + "radian-per-second": "초당 라디안", + "radian-per-second-squared": "초 제곱당 라디안", + "revolutions-per-minute-per-second": "각가속도", + "deg-per-second": "초당 도", + "rotation-per-minute": "분당 회전", + "degrees-brix": "브릭스 도", + "katal": "카탈", + "katal-per-cubic-metre": "세제곱미터당 카탈", + "paris-inch": "파리 인치" }, "user": { "user": "사용자", "users": "사용자", - "customer-users": "커스터머 사용자", + "customer-users": "고객 사용자", "tenant-admins": "테넌트 관리자", "sys-admin": "시스템 관리자", "tenant-admin": "테넌트 관리자", - "customer": "커스터머", + "customer": "고객", "anonymous": "익명", "add": "사용자 추가", "delete": "사용자 삭제", @@ -2056,9 +7173,9 @@ "delete-users-text": "사용자 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "activation-email-sent-message": "활성화 이메일을 보냈습니다!", "resend-activation": "활성화 재전송", - "email": "Email", + "email": "이메일", "email-required": "Email을 입력하세요.", - "invalid-email-format": "잘못된 Email 형식입니다.", + "invalid-email-format": "잘못된 Email 형식입니다.", "first-name": "이름", "last-name": "성", "description": "설명", @@ -2076,22 +7193,27 @@ "activation-link-copied-message": "사용자 활성화 링크가 클립보드로 복사되었습니다.", "details": "상세", "login-as-tenant-admin": "테넌트 관리자로 로그인", - "login-as-customer-user": "커스터머 사용자로 로그인", + "login-as-customer-user": "고객 사용자로 로그인", "search": "사용자 검색", "selected-users": "{ count, plural, =1 {1명 사용자} other {#명 사용자} } 선택됨", "disable-account": "사용자 계정 비활성화", "enable-account": "사용자 계정 활성", "enable-account-message": "사용자 계정이 성공적으로 활성화 되었습니다!", - "disable-account-message": "사용자 계정이 성공적으로 비활성화 되었습니다!" + "disable-account-message": "사용자 계정이 성공적으로 비활성화 되었습니다!", + "copyId": "사용자 ID 복사", + "idCopiedMessage": "사용자 ID가 클립보드에 복사되었습니다.", + "user-list": "사용자 목록", + "user-list-required": "사용자 목록은 필수 항목입니다.", + "printable-one-time-password-hint": "이 인쇄 가능한 일회용 암호는 여행 중처럼 휴대전화를 사용할 수 없을 때 로그인할 수 있도록 해줍니다." }, "value": { "type": "값 유형", "string": "문자열", "string-value": "문자열 값", - "string-value-required": "문자열 값을 입력하세요", + "string-value-required": "문자열 값을 입력하세요", "integer": "정수", "integer-value": "정수 값", - "integer-value-required": "정수 값을 입력하세요", + "integer-value-required": "정수 값을 입력하세요", "invalid-integer-value": "유효하지 않은 정수 값", "double": "실수", "double-value": "실수 값", @@ -2100,24 +7222,109 @@ "boolean-value": "불리언 값", "false": "거짓", "true": "참", - "long": "Long", + "long": "정수", "json": "JSON", "json-value": "JSON 값", "json-value-invalid": "JSON 값의 형식이 잘못되었습니다", "json-value-required": "JSON 값을 입력하세요." }, + "version-control": { + "version-control": "버전 관리", + "management": "버전 관리 관리", + "search": "버전 검색", + "branch": "브랜치", + "default": "기본값", + "select-branch": "브랜치 선택", + "branch-required": "브랜치가 필요합니다", + "create-entity-version": "엔티티 버전 생성", + "version-name": "버전 이름", + "version-name-required": "버전 이름은 필수 항목입니다.", + "author": "작성자", + "export-relations": "관계 내보내기", + "export-attributes": "속성 내보내기", + "export-credentials": "자격 증명 내보내기", + "export-calculated-fields": "계산 필드 및 알람 규칙 내보내기", + "export-alarm-rules": "알람 규칙 내보내기", + "entity-versions": "엔티티 버전", + "versions": "버전", + "created-time": "생성 일시", + "version-id": "버전 ID", + "no-entity-versions-text": "엔티티 버전을 찾을 수 없습니다.", + "no-versions-text": "버전을 찾을 수 없습니다.", + "copy-full-version-id": "전체 버전 ID 복사", + "create-version": "버전 생성", + "creating-version": "버전 생성 중... 잠시 기다려주세요", + "nothing-to-commit": "커밋할 변경 사항 없음", + "restore-version": "버전 복원", + "restore-entity-from-version": "버전 '{{versionName}}'에서 엔티티 복원", + "restoring-entity-version": "엔티티 버전 복원 중... 잠시 기다려주세요", + "load-relations": "관계 로드", + "load-attributes": "속성 로드", + "load-credentials": "자격 증명 로드", + "load-calculated-fields": "계산 필드 및 알람 규칙 로드", + "load-alarm-rules": "알람 규칙 로드", + "compare-with-current": "현재와 비교", + "diff-entity-with-version": "엔티티 버전 '{{versionName}}'과(와) 차이점", + "previous-difference": "이전 차이점", + "next-difference": "다음 차이점", + "current": "현재", + "differences": "{ count, plural, =1 {1개 차이점} other {#개 차이점} }", + "create-entities-version": "엔티티 버전 생성", + "default-sync-strategy": "기본 동기화 전략", + "sync-strategy-merge": "병합", + "sync-strategy-overwrite": "덮어쓰기", + "entities-to-export": "내보낼 엔티티", + "entities-to-restore": "복원할 엔티티", + "sync-strategy": "동기화 전략", + "all-entities": "모든 엔티티", + "no-entities-to-export-prompt": "내보낼 엔티티를 지정하세요", + "no-entities-to-restore-prompt": "복원할 엔티티를 지정하세요", + "add-entity-type": "엔티티 유형 추가", + "remove-all": "모두 제거", + "version-create-result": "{ added, plural, =0 {엔티티 없음} =1 {1개 엔티티} other {#개 엔티티} } 추가됨.
{ modified, plural, =0 {엔티티 없음} =1 {1개 엔티티} other {#개 엔티티} } 수정됨.
{ removed, plural, =0 {엔티티 없음} =1 {1개 엔티티} other {#개 엔티티} } 제거됨.", + "remove-other-entities": "다른 엔티티 제거", + "find-existing-entity-by-name": "이름으로 기존 엔티티 찾기", + "restore-entities-from-version": "버전 '{{versionName}}'에서 엔티티 복원", + "restoring-entities-from-version": "엔티티 복원 중... 잠시 기다려 주세요", + "no-entities-restored": "복원된 엔티티 없음", + "created": "{{created}}개 생성됨", + "updated": "{{updated}}개 업데이트됨", + "deleted": "{{deleted}}개 삭제됨", + "remove-other-entities-confirm-text": "주의! 이 작업은 복원하려는 버전에 없는 모든 현재 엔티티를 영구적으로 삭제합니다.

확인하려면 \"다른 엔티티 제거\"를 입력하세요.", + "auto-commit-to-branch": "{{ branch }} 브랜치에 자동 커밋", + "default-create-entity-version-name": "{{entityName}} 업데이트", + "sync-strategy-merge-hint": "저장소에서 선택한 엔티티를 생성하거나 업데이트합니다. 다른 모든 저장소 엔티티는 수정되지 않습니다.", + "sync-strategy-overwrite-hint": "저장소에서 선택한 엔티티를 생성하거나 업데이트합니다. 다른 모든 저장소 엔티티는 삭제됩니다.", + "device-credentials-conflict": "외부 ID가 {{entityId}}인 장치를 로드하지 못했습니다.
동일한 자격 증명이 이미 다른 장치에 대해 데이터베이스에 있기 때문입니다.
복원 양식에서 자격 증명 로드 설정을 비활성화하는 것을 고려하세요.", + "missing-referenced-entity": "외부 ID가 {{sourceEntityId}}인 {{sourceEntityTypeName}}을(를) 로드하지 못했습니다.
ID가 {{targetEntityId}}인 누락된 {{targetEntityTypeName}}을(를) 참조하기 때문입니다.", + "runtime-failed": "실패: {{message}}", + "auto-commit-settings-read-only-hint": "자동 커밋 기능은 저장소 설정에서 읽기 전용 옵션이 활성화된 경우 작동하지 않습니다.", + "rollback-on-error": "오류 발생 시 롤백", + "rollback-on-error-hint": "복원할 엔티티가 많은 경우 이 옵션을 비활성화하여 성능을 향상시키는 것을 고려하세요.\n 참고: 버전 로드 중에 오류가 발생하면 이미 저장된 엔티티(관계, 속성 등 포함)가 그대로 유지됩니다." + }, "widget": { "widget-library": "위젯 라이브러리", "widget-bundle": "위젯 번들", + "all-bundles": "모든 번들", "select-widgets-bundle": "위젯 번들 선택", + "widgets": "위젯", + "all-widgets": "모든 위젯", + "widget": "위젯", + "select-widget": "위젯 선택", + "no-widgets-matching": "'{{entity}}'과(와) 일치하는 위젯이 없습니다.", + "no-widgets": "아직 위젯이 없음", + "no-widgets-text": "위젯을 찾을 수 없음", "management": "위젯 관리", "editor": "위젯 편집기", + "confirm-to-exit-editor-html": "저장되지 않은 위젯 설정이 있습니다.
이 페이지를 떠나시겠습니까?", "widget-type-not-found": "위젯 구성을 로드하는 중 문제가 발생했습니다.
연결된 위젯 타입이 삭제되었습니다.", "widget-type-load-error": "다음과 같은 오류로 인해 위젯을 불러오지 못했습니다:", "remove": "위젯 삭제", + "delete": "위젯 삭제", "edit": "위젯 수정", "remove-widget-title": "'{{widgetTitle}}' 위젯을 삭제하시겠습니까?", "remove-widget-text": "위젯 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "replace-reference-with-widget-copy": "참조를 위젯 복사본으로 교체", "timeseries": "시계열", "search-data": "데이터 검색", "no-data-found": "아무 데이터도 없습니다", @@ -2125,59 +7332,202 @@ "rpc": "컨트롤 위젯", "alarm": "알람 위젯", "static": "상태 위젯", + "timeseries-short": "시리즈", + "latest-short": "최신", + "rpc-short": "제어", + "alarm-short": "알람", + "static-short": "정적", "select-widget-type": "위젯 타입 선택", "missing-widget-title-error": "위젯 제목을 입력하세요!", "widget-saved": "위젯이 저장되었습니다.", "unable-to-save-widget-error": "위젯을 저장할 수 없습니다! 위젯에 오류가 있습니다!", "save": "위젯 저장", "saveAs": "다른 이름으로 위젯 저장", - "save-widget-type-as": "다른 이름으로 위젯 타입 저장", - "save-widget-type-as-text": "새로운 위젯 이름과 위젯 번들을 선택하세요.", + "move": "위젯 이동", + "save-widget-as": "다른 이름으로 위젯 저장", + "save-widget-as-text": "새 위젯 제목을 입력하세요", "toggle-fullscreen": "전체화면 전환", "run": "위젯 실행", + "widget-title": "위젯 제목", "title": "위젯 제목", "title-required": "위젯 제목을 입력하세요.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "system": "시스템", "type": "위젯 타입", "resources": "리소스", - "resource-url": "JavaScript/CSS URI", + "resource-url": "JavaScript/CSS URL", + "resource-is-extension": "확장 프로그램임", "remove-resource": "리소스 삭제", "add-resource": "리소스 추가", "html": "HTML", - "tidy": "Tidy", + "tidy": "깔끔한", "css": "CSS", - "settings-schema": "스키마 설정", - "datakey-settings-schema": "데이터 키 설정 스키마", - "javascript": "Javascript", + "settings-form": "설정 폼", + "data-key-settings-form": "데이터 키 설정 폼", + "latest-data-key-settings-form": "최신 데이터 키 설정 폼", + "widget-settings": "위젯 설정", + "description": "설명", + "tags": "태그", + "image-preview": "이미지 미리보기", + "settings-form-selector": "설정 폼 선택기", + "data-key-settings-form-selector": "데이터 키 설정 폼 선택기", + "latest-data-key-settings-form-selector": "최신 데이터 키 설정 폼 선택기", + "all": "모두", + "actual": "실제", + "scada": "SCADA 심볼", + "deprecated": "사용 중단됨", + "has-basic-mode": "기본 모드 있음", + "basic-mode-form-selector": "기본 모드 폼 선택기", + "basic-mode": "기본", + "advanced-mode": "고급", + "javascript": "JavaScript", "js": "JS", - "add-widget-type": "새로운 위젯 타입 추가", + "delete-widget-title": "위젯 '{{widgetName}}'을(를) 삭제하시겠습니까?", + "delete-widget-text": "위젯 및 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", + "delete-widgets-title": "{ count, plural, =1 {1개 위젯} other {#개 위젯} }을(를) 삭제하시겠습니까?", + "delete-widgets-text": "주의: 확인 후 선택한 모든 위젯이 제거되고 모든 관련 데이터를 복구할 수 없게 됩니다.", + "delete-widget": "위젯 삭제", "widget-template-load-failed-error": "위젯 템플릿을 로드하지 못했습니다!", + "details": "상세 내역", + "widget-details": "위젯 세부 정보", "add": "위젯 추가", + "add-existing-widget": "기존 위젯 추가", + "add-new-widget": "위젯 추가", + "search-widgets": "위젯 검색", + "selected-widgets": "{ count, plural, =1 {1개 위젯} other {#개 위젯} } 선택됨", "undo": "위젯 변경사항 취소", "export": "위젯 내보내기", - "no-data": "No data to display on widget", - "data-overflow": "Widget displays {{count}} out of {{total}} entities", - "alarm-data-overflow": "Widget displays alarms for {{allowedEntities}} (maximum allowed) entities out of {{totalEntities}} entities" + "export-prompt": "위젯 이미지 및 리소스 포함", + "export-widgets": "위젯 내보내기", + "export-widgets-prompt": "위젯 이미지 및 리소스 포함", + "import": "위젯 가져오기", + "no-data": "위젯에 표시할 데이터 없음", + "data-overflow": "위젯이 {{total}}개 엔티티 중 {{count}}개를 표시합니다", + "alarm-data-overflow": "위젯이 {{totalEntities}}개 엔티티 중 {{allowedEntities}}개(최대 허용)에 대한 알람을 표시합니다", + "search": "위젯 검색", + "filter": "위젯 필터 유형", + "loading-widgets": "위젯 로드 중...", + "widget-template-error": "잘못된 위젯 HTML 템플릿입니다.", + "reference": "참조" }, "widget-action": { "header-button": "위젯 헤더 버튼", + "do-nothing": "아무것도 하지 않음", "open-dashboard-state": "새로운 대시보드 상태 탐색", "update-dashboard-state": "현재 대시보드 상태 업데이트", "open-dashboard": "다른 대시보드 탐색", - "custom": "Custom action", + "custom": "사용자 지정 작업", "custom-pretty": "Custom action (HTML 템플릿)", + "custom-pretty-error-title": "사용자 지정 대화 상자 오류", + "custom-pretty-template-error": "잘못된 사용자 지정 대화 상자 템플릿입니다.", + "custom-pretty-controller-error": "사용자 지정 대화 상자 함수를 평가하는 동안 오류가 발생했습니다.", + "mobile-action": "모바일 작업", "target-dashboard-state": "대상 대시보드 상태", "target-dashboard-state-required": "대상 대시보드 상태가 필요합니다.", "set-entity-from-widget": "위젯으로 부터 객체 설정", "target-dashboard": "대상 대시보드", - "open-right-layout": "Open right dashboard layout (모바일 보기)" + "select-target-dashboard": "대상 대시보드 선택", + "target-dashboard-required": "대상 대시보드가 필요합니다.", + "open-right-layout": "Open right dashboard layout (모바일 보기)", + "state-display-type": "대시보드 상태 표시 옵션", + "open-normal": "정상", + "open-in-separate-dialog": "별도의 대화 상자에서 열기", + "open-in-popover": "팝오버에서 열기", + "dialog-title": "대화 상자 제목", + "dialog-hide-dashboard-toolbar": "대화 상자에서 대시보드 도구 모음 숨기기", + "dialog-width": "뷰포트 너비에 대한 대화 상자 너비 비율", + "dialog-height": "뷰포트 높이에 대한 대화 상자 높이 비율", + "dialog-size-range-error": "대화 상자 크기 비율 값은 1에서 100 사이여야 합니다.", + "popover-preferred-placement": "선호하는 팝오버 배치", + "popover-placement-top": "상단", + "popover-placement-topLeft": "왼쪽 상단", + "popover-placement-topRight": "오른쪽 상단", + "popover-placement-right": "오른쪽", + "popover-placement-rightTop": "오른쪽 상단", + "popover-placement-rightBottom": "오른쪽 하단", + "popover-placement-bottom": "하단", + "popover-placement-bottomLeft": "왼쪽 하단", + "popover-placement-bottomRight": "오른쪽 하단", + "popover-placement-left": "왼쪽", + "popover-placement-leftTop": "왼쪽 상단", + "popover-placement-leftBottom": "왼쪽 하단", + "popover-hide-on-click-outside": "외부 클릭 시 팝오버 숨기기", + "popover-hide-dashboard-toolbar": "팝오버에서 대시보드 도구 모음 숨기기", + "popover-width": "팝오버 너비", + "popover-height": "팝오버 높이", + "popover-style": "팝오버 스타일", + "open-new-browser-tab": "새 브라우저 탭에서 열기", + "open-URL": "URL 열기", + "URL": "URL", + "url-required": "URL을(를) 입력하세요.", + "mobile": { + "device-provision": "장치 프로비저닝", + "action-type": "모바일 작업 유형", + "select-action-type": "모바일 작업 유형 선택", + "action-type-required": "모바일 작업 유형이 필요합니다", + "take-picture-from-gallery": "갤러리에서 사진 가져오기", + "take-photo": "사진 촬영", + "map-direction": "지도 경로 열기", + "map-location": "지도 위치 열기", + "scan-qr-code": "QR 코드 스캔", + "make-phone-call": "전화 걸기", + "get-location": "휴대전화 위치 가져오기", + "take-screenshot": "스크린샷 촬영", + "handle-provision-success-function": "프로비저닝 성공 처리 함수", + "get-location-function": "위치 가져오기 함수", + "process-launch-result-function": "실행 결과 처리 함수", + "get-phone-number-function": "전화번호 가져오기 함수", + "process-image-function": "이미지 처리 함수", + "process-qr-code-function": "QR 코드 처리 함수", + "process-location-function": "위치 처리 함수", + "handle-empty-result-function": "빈 결과 처리 함수", + "handle-error-function": "오류 처리 함수", + "handle-non-mobile-fallback-function": "비모바일 폴백 처리 함수", + "save-to-gallery": "갤러리에 저장", + "provision-type": "프로비저닝 유형", + "auto": "자동", + "wi-fi": "와이파이", + "ble": "BLE", + "soft-ap": "소프트 AP" + }, + "custom-action-function": "사용자 지정 작업 함수", + "custom-pretty-function": "사용자 지정 작업 (HTML 템플릿 포함) 함수", + "map-item-type": "지도 항목 유형", + "map-item": { + "marker": "마커", + "polygon": "다각형", + "rectangle": "사각형", + "circle": "원", + "polyline": "폴리라인" + }, + "place-map-item": "지도 항목 배치", + "map-item-tooltip": { + "customize-map-item-tooltips": "지도 항목 툴팁 사용자 지정", + "place-marker": "마커 배치", + "start-draw-rectangle": "사각형 그리기 시작", + "finish-draw-rectangle": "사각형 그리기 완료", + "start-draw-polygon": "다각형 그리기 시작", + "continue-draw-polygon": "다각형 그리기 계속", + "finish-draw-polygon": "다각형 그리기 완료", + "start-draw-circle": "원 그리기 시작", + "finish-draw-circle": "원 그리기 완료", + "start-draw-polyline": "폴리라인 그리기 시작", + "finish-draw-polyline": "폴리라인 그리기 완료" + } }, "widgets-bundle": { "current": "현재 번들", "widgets-bundles": "위젯 번들", + "widgets-bundle-widgets": "위젯 번들 위젯", "add": "위젯 번들 추가", "delete": "위젯 번들 삭제", "title": "제목", "title-required": "제목을 입력하세요.", + "title-max-length": "제목은 256자 미만이어야 합니다.", + "description": "설명", + "image-preview": "이미지 미리보기", + "scada": "SCADA 위젯 번들", + "order": "순서", "add-widgets-bundle-text": "위젯 번들 추가", "no-widgets-bundles-text": "위젯 번들이 없습니다.", "empty": "위젯 번들이 비어있습니다.", @@ -2193,41 +7543,63 @@ "system": "시스템", "import": "위젯 번들 가져오기", "export": "위젯 번들 내보내기", + "export-widgets-bundle-widgets-prompt": "내보낸 데이터에 번들 위젯 포함 (그렇지 않으면 참조된 위젯 FQN만 내보내짐)", "export-failed-error": "위젯 번들을 내보내기 할 수 없습니다.: {{error}}", "create-new-widgets-bundle": "새로운 위젯 번들 생성", "widgets-bundle-file": "위젯 번들 파일", "invalid-widgets-bundle-file-error": "위젯 번들을 가져오기 할 수 없습니다.: 잘못된 위젯 번들 데이터 구조입니다.", "search": "위젯 번들 검색", "selected-widgets-bundles": "{ count, plural, =1 {1 개 위젯 번들} other {# 개 위젯 번들} } 선택됨", - "open-widgets-bundle": "위젯 번들 열기" + "open-widgets-bundle": "위젯 번들 열기", + "loading-widgets-bundles": "위젯 번들 로드 중...", + "create-new": "새 위젯 번들 만들기" }, "widget-config": { "data": "데이터", "settings": "설정", "advanced": "고급", + "appearance": "외관", + "widget-card": "위젯 카드", + "mobile": "모바일", "title": "제목", "title-tooltip": "제목 툴팁", "general-settings": "일반 설정", "display-title": "제목 표시", + "card-title": "카드 제목", "drop-shadow": "그림자", "enable-fullscreen": "전체화면 사용 ", "background-color": "배경 색", "text-color": "글자 색", + "border-radius": "테두리 반경", "padding": "패딩", "margin": "여백", "widget-style": "위젯 스타일", + "widget-css": "위젯 CSS", "title-style": "제목 스타일", "mobile-mode-settings": "모바일 모드 설정", "order": "순서", "height": "높이", + "mobile-hide": "모바일 모드에서 위젯 숨기기", + "desktop-hide": "데스크톱 모드에서 위젯 숨기기", "units": "값 옆에 표시할 특수 기호", + "units-by-default": "기본 단위", "decimals": "소수점 이하 자릿수", + "decimals-by-default": "기본 소수 자릿수", + "default-data-key-parameter-hint": "이 매개변수는 데이터 키 구성에 의해 재정의되지 않는 한 모든 위젯 값에 적용됩니다", + "units-short": "단위", + "decimals-short": "소수 자릿수", + "decimals-suffix": "소수 자릿수", + "digits-suffix": "자릿수", "timewindow": "타임윈도우", "use-dashboard-timewindow": "대시보드 타임윈도우", + "use-widget-timewindow": "위젯 시간 창 사용", "display-timewindow": "타임윈도우 표시", + "legend": "범례", "display-legend": "범례 표시", "datasources": "데이터 소스", - "maximum-datasources": "최대 { count, plural, =1 {1 개의 데이터 소스만 허용됩니다.} other {# 개의 데이터 소스만 허용됩니다.} }", + "datasource": "데이터소스", + "maximum-datasources": "최대 { count, plural, =1 {1 개의 데이터 소스만 허용됩니다.} other {# 개의 데이터 소스만 허용됩니다.} }", + "timeseries-key-error": "최소 하나의 시계열 데이터 키를 지정해야 합니다", "datasource-type": "유형", "datasource-parameters": "파라미터", "remove-datasource": "데이터소스 삭제", @@ -2240,31 +7612,548 @@ "search-actions": "액션 검색", "no-actions-text": "아무 액션도 없습니다", "action-source": "액션 소스", + "select-action-source": "작업 소스 선택", "action-source-required": "액션 소스를 입력하세요.", + "column-index": "열 인덱스", + "select-column-index": "열 인덱스 선택", + "column-index-required": "열 인덱스가 필요합니다.", + "not-set": "설정되지 않음", "action-name": "이름", "action-name-required": "액션 이름을 입력하세요.", "action-name-not-unique": "같은 이름의 액션이 이미 존재합니다.\n같은 액션 소스에서 액션 이름이 중복될 수 없습니다.", "action-icon": "아이콘", + "header-button": { + "button-settings": "버튼 설정", + "button-type": "버튼 유형", + "button-type-basic": "기본", + "button-type-raised": "양각", + "button-type-stroked": "윤곽선", + "button-type-flat": "평면", + "button-type-icon": "아이콘", + "button-type-mini-fab": "플로팅 버튼", + "colors": "색상", + "color": "색상", + "background": "배경", + "border": "테두리", + "advanced-button-style": "고급 버튼 스타일", + "button-style": "버튼 스타일" + }, + "show-hide-action-using-function": "함수를 사용하여 작업 표시/숨기기", + "show-action-function": "작업 표시 함수", "action-type": "유형", "action-type-required": "액션 유형을 입력하세요.", "edit-action": "액션 편집", "delete-action": "액션 삭제", "delete-action-title": "위젯 액션 삭제", "delete-action-text": "위젯 액션 '{{actionName}}'을(를) 삭제하시겠습니까?", + "title-icon": "제목 아이콘", "display-icon": "제목 아이콘 표시", + "card-icon": "카드 아이콘", + "icon": "아이콘", "icon-color": "아이콘 색상", - "icon-size": "아이콘 크기" + "icon-size": "아이콘 크기", + "advanced-settings": "고급 설정", + "data-settings": "데이터 설정", + "limits": "제한", + "no-data-display-message": "\"표시할 데이터 없음\" 대체 메시지", + "data-page-size": "데이터 소스당 최대 엔티티 수", + "settings-component-not-found": "선택기 '{{selector}}'에 대한 설정 폼 구성 요소를 찾을 수 없습니다", + "preview": "미리보기", + "set": "설정", + "set-message": "메시지 설정", + "advanced-title-style": "고급 제목 스타일", + "card-style": "카드 스타일", + "text": "텍스트", + "background": "배경", + "advanced-widget-style": "고급 위젯 스타일", + "card-buttons": "카드 버튼", + "show-card-buttons": "카드 버튼 표시", + "card-border-radius": "카드 테두리 반경", + "card-padding": "카드 패딩", + "card-appearance": "카드 외관", + "color": "색상", + "tooltip": "툴팁", + "units-required": "단위는 필수 항목입니다.", + "list-layout": "목록 레이아웃", + "layout": "레이아웃", + "resize-options": "크기 조정 옵션", + "resizable": "크기 조정 가능", + "preserve-aspect-ratio": "가로세로 비율 유지" }, "widget-type": { "import": "위젯 타입 가져오기", "export": "위젯 타입 내보내기", "export-failed-error": "위젯 타입을 내보내기 할 수 없습니다.: {{error}}", - "create-new-widget-type": "새로운 위젯 타입 생성", - "widget-type-file": "위젯 타입 파일", - "invalid-widget-type-file-error": "위젯 타입을 가져오기 할 수 없습니다.: 잘못된 위젯 타입 데이터 구조입니다." + "widget-file": "위젯 파일", + "invalid-widget-file-error": "위젯 가져오기를 할 수 없습니다: 위젯 데이터 구조가 잘못되었습니다." }, "widgets": { + "mobile-app-qr-code": { + "configuration-hint": "구성은 플랫폼 기본 설정의 모바일 앱 QR 코드 위젯에 따라 달라집니다", + "get-it-on-google-play": "Google Play에서 받기", + "download-on-the-app-store": "App Store에서 다운로드" + }, + "action-button": { + "behavior": "동작", + "on-click": "클릭 시", + "on-click-hint": "버튼을 클릭할 때 트리거되는 동작", + "first-button-click": "첫 번째 버튼 클릭", + "first-button-click-hint": "첫 번째 버튼을 누르는 동안 동작.", + "second-button-click": "두 번째 버튼 클릭", + "second-button-click-hint": "두 번째 버튼을 누르는 동안 동작.", + "button-click-hint": "위젯을 누르는 동안 동작." + }, + "command-button": { + "behavior": "동작", + "on-click": "클릭 시", + "on-click-hint": "버튼을 클릭할 때 수행되는 동작." + }, + "power-button": { + "behavior": "동작", + "power-on": "전원 '켜기'", + "power-on-hint": "구성 요소의 전원을 켜기 위해 수행되는 동작.", + "power-off": "전원 '끄기'", + "power-off-hint": "구성 요소의 전원을 끄기 위해 수행되는 동작.", + "on-label": "켜기", + "off-label": "끄기", + "layout": "레이아웃", + "layout-default": "기본값", + "layout-simplified": "간소화", + "layout-outlined": "윤곽선", + "layout-default-volume": "기본.볼륨", + "layout-simplified-volume": "간소화.볼륨", + "layout-outlined-volume": "윤곽선.볼륨", + "layout-default-icon": "기본.아이콘", + "layout-simplified-icon": "간소화.아이콘", + "layout-outlined-icon": "윤곽선.아이콘", + "main": "메인", + "background": "배경", + "button-icon-on": "버튼 아이콘 '켜기'", + "button-icon-off": "버튼 아이콘 '끄기'", + "power-on-colors": "전원 '켜기' 색상", + "power-off-colors": "전원 '끄기' 색상", + "disabled-colors": "비활성화 색상", + "button": "버튼" + }, + "toggle-button": { + "behavior": "동작", + "checked": "선택됨", + "unchecked": "선택 해제됨", + "check": "선택", + "check-hint": "구성 요소를 선택하기 위해 수행되는 동작.", + "uncheck": "선택 해제", + "uncheck-hint": "구성 요소를 선택 해제하기 위해 수행되는 동작.", + "auto-scale": "자동 크기 조정", + "horizontal-fill": "가로 채우기", + "vertical-fill": "세로 채우기", + "button-appearance": "버튼 외관" + }, + "segmented-button": { + "layout": "레이아웃", + "layout-squared": "사각형", + "layout-rounded": "둥근", + "card-border": "카드 테두리", + "button-appearance": "버튼 외관", + "first": "첫 번째", + "second": "초", + "color-styles": "색상 스타일", + "selected": "선택됨", + "unselected": "선택되지 않음" + }, + "button": { + "layout": "레이아웃", + "outlined": "윤곽선", + "filled": "채워짐", + "underlined": "밑줄", + "basic": "기본", + "auto-scale": "자동 크기 조정", + "label": "라벨", + "icon": "아이콘", + "border-radius": "테두리 반경", + "color-palette": "색상 팔레트", + "main": "메인", + "background": "배경", + "border": "테두리", + "custom-styles": "사용자 지정 스타일", + "clear-style": "스타일 지우기", + "shadow": "그림자", + "enabled": "사용", + "disabled": "비활성화됨", + "preview": "미리보기", + "copy-style-from": "스타일 복사 원본" + }, + "value-stepper": { + "behavior": "동작", + "simplified": "간소화", + "filled": "채워짐", + "outlined": "윤곽선", + "volume": "부피", + "initial-state": "초기 상태", + "initial-state-hint": "초기 값을 가져오는 동작.", + "disabled-state": "비활성화 상태", + "disabled-state-hint": "구성 요소가 비활성화되는 조건을 구성합니다.", + "right-button-click": "오른쪽 버튼 클릭", + "right-button-click-hint": "오른쪽 버튼을 누르는 동안 동작.", + "left-button-click": "왼쪽 버튼 클릭", + "left-button-click-hint": "왼쪽 버튼을 누르는 동안 동작.", + "auto-scale": "자동 크기 조정", + "value-range": "범위", + "min-range": "최소", + "max-range": "최대", + "value-increment-decrement-step": "값 증가/감소 단계", + "value": "값", + "value-box-background": "값 상자 배경", + "border": "테두리", + "button-appearance": "버튼 외관", + "left": "왼쪽", + "right": "오른쪽", + "left-button": "왼쪽 버튼", + "right-button": "오른쪽 버튼", + "icon": "아이콘", + "color-palette": "색상 팔레트", + "main": "메인", + "background": "배경", + "button-icon-on": "버튼 아이콘 '켜기'", + "button-on-colors": "전원 '켜기' 색상", + "disabled-colors": "비활성화 색상" + }, + "button-state": { + "activated-state": "활성화 상태", + "activated-state-hint": "버튼이 활성화되는 조건을 구성합니다.", + "disabled-state": "비활성화 상태", + "disabled-state-hint": "버튼이 비활성화되는 조건을 구성합니다.", + "selected-state": "선택 버튼", + "selected-state-hint": "버튼이 선택되는 조건을 구성합니다.", + "enabled": "사용", + "hovered": "호버됨", + "pressed": "눌림", + "activated": "활성", + "disabled": "비활성화됨", + "initial": "첫 번째 버튼", + "first": "첫 번째", + "second": "초" + }, + "background": { + "background": "배경", + "background-settings": "배경 설정", + "background-type-image": "이미지", + "background-type-color": "색상", + "image-url": "이미지 URL", + "overlay": "오버레이", + "enable-overlay": "오버레이 활성화", + "blur": "흐림", + "preview": "미리보기" + }, + "bar-chart": { + "bar-appearance": "막대 외관", + "label-on-bar": "막대에 레이블", + "value-on-bar": "막대에 값", + "bar-chart-style": "막대 차트 스타일", + "bar-axis": "막대 축" + }, + "polar-area-chart": { + "polar-axis": "극축", + "start-angle": "시작 각도", + "polar-area-chart-style": "극 영역 차트 스타일" + }, + "battery-level": { + "layout": "레이아웃", + "layout-vertical-solid": "수직. 실선", + "layout-horizontal-solid": "수평. 실선", + "layout-vertical-divided": "수직. 분할", + "layout-horizontal-divided": "수평. 분할", + "icon": "아이콘", + "value": "값", + "auto-scale": "자동 크기 조정", + "battery-level-color": "배터리 레벨 색상", + "battery-shape-color": "배터리 모양 색상", + "battery-level-card-style": "배터리 레벨 카드 스타일", + "sections-count": "섹션 수" + }, + "signal-strength": { + "value": "값", + "last-update": "마지막 업데이트", + "no-signal": "신호 없음", + "layout": "레이아웃", + "layout-wifi": "와이파이", + "layout-cellular-bar": "셀룰러 막대", + "icon": "아이콘", + "date": "날짜", + "active-bars-color": "활성 신호 막대 색상", + "inactive-bars-color": "비활성 신호 막대 색상", + "signal-strength-card-style": "신호 강도 카드 스타일", + "no-signal-rssi-value": "\"신호 없음\" RSSI 값" + }, + "status-widget": { + "behavior": "동작", + "layout": "레이아웃", + "layout-default": "기본값", + "layout-center": "중앙", + "layout-icon": "아이콘", + "on": "켜기", + "off": "끄기", + "label": "라벨", + "status": "상태", + "icon": "아이콘", + "color-palette": "색상 팔레트", + "disabled-color-palette": "비활성화 색상 팔레트", + "primary": "기본", + "primary-color-hint": "아이콘 및 레이블 색상", + "secondary": "보조", + "secondary-color-hint": "상태 색상", + "background": "배경" + }, + "chart": { + "common-settings": "일반 설정", + "enable-stacking-mode": "스택 모드 활성화", + "selection": "시간 범위 선택", + "enable-selection-mode": "선택 모드 활성화", + "line-shadow-size": "선 그림자 크기", + "display-smooth-lines": "부드러운 (곡선) 선 표시", + "default-bar-width": "비집계 데이터의 기본 막대 너비 (밀리초)", + "bar-alignment": "막대 정렬", + "bar-alignment-left": "왼쪽", + "bar-alignment-right": "오른쪽", + "bar-alignment-center": "중앙", + "default-font": "기본 글꼴", + "default-font-size": "기본 글꼴 크기", + "default-font-color": "기본 글꼴 색상", + "thresholds-line-width": "모든 임계값의 기본 선 너비", + "tooltip-settings": "툴팁 설정", + "tooltip": "툴팁", + "show-tooltip": "툴팁 표시", + "hover-individual-points": "개별 지점 호버", + "show-cumulative-values": "스택 모드에서 누적 값 표시", + "hide-zero-false-values": "툴팁에서 0/false 값 숨기기", + "tooltip-value-format-function": "툴팁 값 형식 함수", + "grid-settings": "그리드 설정", + "show-vertical-lines": "수직선 표시", + "show-horizontal-lines": "수평선 표시", + "grid-outline-border-width": "그리드 윤곽선/테두리 너비 (px)", + "primary-color": "기본 색상", + "background-color": "배경색", + "ticks-color": "눈금 색상", + "xaxis-settings": "X축 설정", + "axis-title": "축 제목", + "xaxis-tick-labels-settings": "X축 눈금 레이블 설정", + "show-tick-labels": "축 눈금 레이블 표시", + "yaxis-settings": "Y축 설정", + "min-scale-value": "눈금의 최소 값", + "max-scale-value": "눈금의 최대 값", + "yaxis-tick-labels-settings": "Y축 눈금 레이블 설정", + "tick-step-size": "눈금 사이의 단계 크기", + "number-of-decimals": "표시할 소수 자릿수", + "ticks-formatter-function": "눈금 형식 함수", + "comparison-settings": "비교 설정", + "enable-comparison": "비교 활성화", + "time-for-comparison": "비교 기간", + "time-for-comparison-previous-interval": "이전 간격 (기본값)", + "time-for-comparison-days": "1일 전", + "time-for-comparison-weeks": "1주 전", + "time-for-comparison-months": "1개월 전", + "time-for-comparison-years": "1년 전", + "time-for-comparison-custom-interval": "사용자 지정 간격", + "custom-interval-value": "사용자 지정 간격 값 (ms)", + "comparison-x-axis-settings": "비교 X축 설정", + "axis-position": "축 위치", + "axis-position-top": "위 (기본값)", + "axis-position-bottom": "하단", + "custom-legend-settings": "사용자 지정 범례 설정", + "enable-custom-legend": "사용자 지정 범례 활성화 (키 레이블에 속성/시계열 값을 사용할 수 있음)", + "key-name": "키 이름", + "key-name-required": "키 이름은 필수 항목입니다.", + "key-type": "키 유형", + "key-type-attribute": "속성", + "key-type-timeseries": "시계열", + "label-keys-list": "레이블에 사용할 키 목록", + "no-label-keys": "구성된 키 없음", + "add-label-key": "새 키 추가", + "line-width": "선 너비", + "color": "색상", + "data-is-hidden-by-default": "데이터가 기본적으로 숨겨짐", + "disable-data-hiding": "데이터 숨기기 비활성화", + "remove-from-legend": "범례에서 데이터 키 제거", + "exclude-from-stacking": "스택에서 제외 (\"스택\" 모드에서 사용 가능)", + "line-settings": "선 설정", + "show-line": "선 표시", + "fill-line": "선 채우기", + "fill-line-opacity": "채우기 불투명도", + "points-settings": "포인트 설정", + "show-points": "지점 표시", + "points-line-width": "포인트 선 너비", + "points-radius": "포인트 반지름", + "point-shape": "포인트 모양", + "point-shape-circle": "원", + "point-shape-cross": "십자가", + "point-shape-diamond": "다이아몬드", + "point-shape-square": "사각형", + "point-shape-triangle": "삼각형", + "point-shape-custom": "사용자 지정 함수", + "point-shape-draw-function": "포인트 모양 그리기 함수", + "show-separate-axis": "별도의 축 표시", + "axis-position-left": "왼쪽", + "axis-position-right": "오른쪽", + "thresholds": "임계값", + "no-thresholds": "구성된 임계값 없음", + "add-threshold": "임계값 추가", + "show-values-for-comparison": "비교를 위한 과거 값 표시", + "comparison-values-label": "과거 값 레이블", + "comparison-line-color": "비교 선 색상", + "threshold-settings": "임계값 설정", + "use-as-threshold": "키 값을 임계값으로 사용", + "threshold-line-width": "임계값 선 너비", + "threshold-color": "임계값 색상", + "common-pie-settings": "공통 원형 차트 설정", + "radius": "반경", + "inner-radius": "내부 반지름", + "tilt": "기울기", + "common-pie-settings-range-error": "값은 0에서 1 사이여야 합니다", + "stroke-settings": "획 설정", + "width-pixels": "너비 (픽셀)", + "show-labels": "레이블 표시", + "animation-settings": "애니메이션 설정", + "animated-pie": "원형 차트 애니메이션 활성화 (실험적)", + "border-settings": "테두리 설정", + "border-width": "테두리 너비", + "border-color": "테두리 색상", + "legend-settings": "범례 설정", + "display-legend": "범례 표시", + "labels-font-color": "레이블 글꼴 색상", + "series": "시리즈", + "add-series": "시리즈 추가", + "series-settings": "시리즈 설정", + "remove-series": "시리즈 제거", + "no-series": "구성된 시리즈 없음", + "no-series-error": "최소 하나의 시리즈를 지정해야 합니다", + "chart-appearance": "차트 모양", + "vertical-grid-lines": "수직 격자선", + "horizontal-grid-lines": "수평 격자선", + "chart-background": "차트 배경", + "grid-lines-color": "격자선 색상", + "border": "테두리", + "axis": "축", + "vertical-axis": "수직 축", + "ticks": "눈금", + "horizontal-axis": "수평 축", + "shape-empty-circle": "빈 원", + "shape-circle": "원", + "shape-rect": "사각형", + "shape-round-rect": "둥근 사각형", + "shape-triangle": "삼각형", + "shape-diamond": "다이아몬드", + "shape-pin": "핀", + "shape-arrow": "화살표", + "shape-none": "없음", + "line-type-solid": "실선", + "line-type-dashed": "파선", + "line-type-dotted": "점선", + "label-position-top": "상단", + "label-position-bottom": "하단", + "label-position-outside": "외부", + "label-position-inside": "내부", + "fill": "채우기", + "fill-type-none": "없음", + "fill-type-solid": "실선", + "fill-type-opacity": "불투명도", + "fill-type-gradient": "그라데이션", + "background": "배경", + "opacity": "불투명도", + "gradient-stops": "그라데이션 중지점", + "gradient-start": "시작", + "gradient-end": "종료", + "animation": { + "animation": "애니메이션", + "animation-threshold": "애니메이션 임계값", + "animation-duration": "애니메이션 지속 시간", + "animation-easing": "애니메이션 완화", + "animation-delay": "애니메이션 지연", + "update-animation-duration": "업데이트 애니메이션 지속 시간", + "update-animation-easing": "업데이트 애니메이션 완화", + "update-animation-delay": "업데이트 애니메이션 지연" + }, + "chart-axis": { + "limit": "제한", + "source": "소스", + "key-value": "키 / 값", + "value-required": "값이 필요합니다.", + "entity-key-required": "엔티티 키는 필수 항목입니다.", + "key-required": "키는 필수 항목입니다.", + "scale-limits": "스케일 제한", + "scale-appearance": "스케일 모양", + "scale": "눈금", + "scale-min": "최소", + "scale-max": "최대", + "scale-auto": "자동" + }, + "bar": { + "show-border": "테두리 표시", + "border-width": "테두리 너비", + "border-radius": "테두리 반경", + "bar-width": "막대 너비", + "label": "라벨", + "label-hint": "막대 위에 레이블 표시.", + "series-label-hint": "막대 위에 값이 있는 레이블 표시.", + "label-background": "레이블 배경" + } + }, + "color": { + "color-settings": "색상 설정", + "color-type-constant": "상수", + "color-type-gradient": "그라데이션", + "color-type-range": "범위", + "color-type-function": "함수", + "color": "색상", + "value-range": "값 범위", + "from": "부터", + "to": "까지", + "color-function": "색상 함수", + "copy-color-settings-from": "색상 설정 복사 위치", + "copy-from": "복사 원본", + "settings-type": "설정 유형", + "basic-mode": "기본", + "advanced-mode": "고급", + "entity-alias": "엔티티 별명", + "entity-attribute": "엔티티 속성", + "gradient-color": "그라데이션 색상", + "gradient-color-min": "색상", + "gradient-start": "그라데이션 시작 색상", + "gradient-start-min": "시작", + "gradient-end": "그라데이션 종료 색상", + "gradient-end-min": "끝", + "start-value": "시작 값", + "end-value": "종료 값", + "gradient-type": "그라데이션 유형" + }, + "dashboard-state": { + "dashboard-state-settings": "대시보드 상태 설정", + "dashboard-state": "대시보드 상태 ID", + "autofill-state-layout": "기본적으로 상태 레이아웃 높이 자동 채우기", + "default-margin": "기본 위젯 여백", + "default-background-color": "기본 배경색", + "sync-parent-state-params": "상태 매개변수를 부모 대시보드와 동기화" + }, "date-range-navigator": { + "date-range-picker-settings": "날짜 범위 선택기 설정", + "hide-date-range-picker": "날짜 범위 선택기 숨기기", + "picker-one-panel": "날짜 범위 선택기 단일 패널", + "picker-auto-confirm": "날짜 범위 선택기 자동 확인", + "picker-show-template": "날짜 범위 선택기 템플릿 표시", + "first-day-of-week": "주의 첫 번째 요일", + "interval-settings": "간격 설정", + "hide-interval": "간격 숨기기", + "initial-interval": "초기 간격", + "interval-hour": "시간", + "interval-day": "일", + "interval-week": "주", + "interval-two-weeks": "2주", + "interval-month": "달", + "interval-three-months": "3개월", + "interval-six-months": "6개월", + "step-settings": "단계 설정", + "hide-step-size": "단계 크기 숨기기", + "initial-step-size": "초기 단계 크기", + "hide-labels": "레이블 숨기기", + "use-session-storage": "세션 저장소 사용", "localizationMap": { "Sun": "일", "Mon": "월", @@ -2321,6 +8210,228 @@ "Ok": "확인" } }, + "doughnut": { + "doughnut-appearance": "도넛 모양", + "layout": "레이아웃", + "layout-default": "기본값", + "layout-with-total": "합계 포함", + "central-total-value": "중앙 합계 값", + "doughnut-card-style": "도넛 카드 스타일" + }, + "entities-hierarchy": { + "hierarchy-data-settings": "계층 데이터 설정", + "relations-query-function": "노드 관계 쿼리 함수", + "has-children-function": "노드에 자식 있음 함수", + "node-state-settings": "노드 상태 설정", + "node-opened-function": "기본 노드 열림 함수", + "node-disabled-function": "노드 비활성화 함수", + "display-settings": "표시 설정", + "node-icon-function": "노드 아이콘 함수", + "node-text-function": "노드 텍스트 함수", + "sort-settings": "정렬 설정", + "nodes-sort-function": "노드 정렬 함수" + }, + "edge": { + "display-default-title": "기본 제목 표시" + }, + "gateway": { + "general-settings": "일반 설정", + "widget-title": "위젯 제목", + "default-archive-file-name": "기본 아카이브 파일 이름", + "device-type-for-new-gateway": "새 게이트웨이에 대한 장치 유형", + "messages-settings": "메시지 설정", + "save-config-success-message": "게이트웨이 구성이 성공적으로 저장되었다는 텍스트 메시지", + "device-name-exists-message": "입력한 이름의 장치가 이미 존재할 때 텍스트 메시지", + "gateway-title": "게이트웨이 양식", + "read-only": "읽기 전용", + "events-title": "게이트웨이 이벤트 양식 제목", + "events-filter": "이벤트 필터", + "event-key-contains": "이벤트 키 포함...", + "show-connector": "커넥터에 표시", + "connector-state-param-key": "커넥터 상태 매개변수 키", + "message": "메시지", + "level": "수준", + "created-time": "생성 일시" + }, + "gauge": { + "default-color": "기본 색상", + "radial-gauge-settings": "방사형 게이지 설정", + "ticks-settings": "눈금 설정", + "min-value": "최소값", + "max-value": "최대값", + "min-value-short": "최소", + "max-value-short": "최대", + "start-ticks-angle": "시작 눈금 각도", + "ticks-angle": "눈금 각도", + "major-ticks": "주 눈금", + "major-ticks-count": "주 눈금 수", + "major-ticks-color": "주 눈금 색상", + "minor-ticks": "보조 눈금", + "minor-ticks-count": "보조 눈금 수", + "minor-ticks-color": "보조 눈금 색상", + "tick-numbers-font": "눈금 숫자 글꼴", + "unit-title-settings": "단위 제목 설정", + "show-unit-title": "단위 제목", + "unit-title": "단위 제목", + "title-font": "제목 텍스트 글꼴", + "units-settings": "단위 설정", + "units-font": "단위 텍스트 글꼴", + "value-box-settings": "값 상자 설정", + "show-value-box": "값 상자 표시", + "value-box": "값 상자", + "value-int": "값의 정수 부분 자릿수", + "value-text": "값 텍스트", + "value-text-shadow": "값 텍스트 그림자", + "value-font": "값 텍스트 글꼴", + "rect-stroke-color-start": "사각형 획 색상 - 시작 그라데이션", + "rect-stroke-color-end": "사각형 획 색상 - 종료 그라데이션", + "background-color": "배경색", + "shadow-color": "그림자 색상", + "value-box-rect-stroke-color": "값 상자 사각형 획 색상", + "value-box-rect-stroke-color-end": "값 상자 사각형 획 색상 - 종료 그라데이션", + "value-box-background-color": "값 상자 배경색", + "value-box-shadow-color": "값 상자 그림자 색상", + "plate-settings": "플레이트 설정", + "show-plate-border": "플레이트 테두리", + "plate-color": "플레이트 색상", + "needle-settings": "바늘 설정", + "needle-circle-size": "바늘 원 크기", + "needle-color": "바늘 색상", + "needle-color-start": "바늘 색상 - 시작 그라데이션", + "needle-color-end": "바늘 색상 - 종료 그라데이션", + "needle-color-shadow-up": "바늘 상반부 그림자 색상", + "needle-color-shadow-down": "그림자", + "highlights-settings": "하이라이트 설정", + "highlights-width": "하이라이트 너비", + "highlights": "하이라이트", + "highlight-from": "부터", + "highlight-to": "까지", + "highlight-color": "색상", + "no-highlights": "구성된 하이라이트 없음", + "add-highlight": "하이라이트 추가", + "animation-settings": "애니메이션 설정", + "enable-animation": "애니메이션", + "animation-duration-rule": "애니메이션 지속 시간 및 규칙", + "animation-duration": "애니메이션 지속 시간", + "animation-rule": "애니메이션 규칙", + "animation-linear": "선형", + "animation-quad": "2차", + "animation-quint": "5차", + "animation-cycle": "사이클", + "animation-bounce": "바운스", + "animation-elastic": "탄성", + "animation-dequad": "역2차", + "animation-dequint": "역5차", + "animation-decycle": "역사이클", + "animation-debounce": "역바운스", + "animation-delastic": "역탄성", + "linear-gauge-settings": "선형 게이지 설정", + "bar-stroke": "막대 획", + "bar-stroke-width": "막대 획 너비", + "bar-stroke-color": "막대 획 색상", + "bar-background-color": "막대 배경색 - 시작 그라데이션", + "bar-background-color-end": "막대 배경색 - 끝 그라데이션", + "progress-bar-color": "진행 막대 색상", + "progress-bar": "진행 막대", + "progress-bar-color-start": "진행 막대 색상 - 시작 그라데이션", + "progress-bar-color-end": "진행 막대 색상 - 끝 그라데이션", + "major-ticks-names": "주요 눈금 이름", + "show-stroke-ticks": "눈금 획 표시", + "major-ticks-font": "주요 눈금 글꼴", + "border-color": "테두리 색상", + "border-width": "테두리 너비", + "needle-circle": "바늘 원", + "needle-circle-color": "바늘 원 색상", + "animation-target": "애니메이션 대상", + "animation-target-needle": "바늘", + "animation-target-plate": "플레이트", + "common-settings": "일반 게이지 설정", + "gauge-type": "게이지 유형", + "gauge-type-arc": "호", + "gauge-type-donut": "도넛", + "gauge-type-horizontal-bar": "수평 막대", + "gauge-type-vertical-bar": "수직 막대", + "donut-start-angle": "시작 각도 (도)", + "bar-settings": "게이지 막대 설정", + "relative-bar-width": "상대적 막대 너비", + "neon-glow-brightness": "네온 글로우 효과 밝기 (0-100)", + "neon-glow-brightness-hint": "0 - 효과 비활성화", + "stripes-thickness": "줄무늬 두께", + "stripes-thickness-hint": "0 - 줄무늬 없음", + "rounded-line-cap": "둥근 선 끝", + "bar-color-settings": "막대 색상 설정", + "use-precise-level-color-values": "정밀 색상 레벨 사용", + "bar-colors": "막대 색상, 아래에서 위로", + "color": "색상", + "no-bar-colors": "구성된 막대 색상 없음", + "add-bar-color": "막대 색상 추가", + "from": "부터", + "to": "까지", + "fixed-level-colors": "경계 값을 사용한 막대 색상", + "gauge-title-settings": "게이지 제목 설정", + "show-gauge-title": "게이지 제목 표시", + "gauge-title": "게이지 제목", + "gauge-title-font": "게이지 제목 글꼴", + "unit-title-and-timestamp-settings": "단위 제목 및 타임스탬프 설정", + "show-timestamp": "타임스탬프", + "timestamp-format": "타임스탬프 형식", + "label-font": "값 아래에 표시되는 레이블 글꼴", + "value-settings": "값 설정", + "show-value": "값 텍스트 표시", + "min-max-settings": "최소/최대 레이블 설정", + "show-min-max": "최소 및 최대 값 표시", + "min-max-font": "최소 및 최대 레이블 글꼴", + "show-ticks": "눈금 표시", + "tick-width": "눈금 너비", + "tick-color": "눈금 색상", + "tick-values": "눈금 값", + "no-tick-values": "구성된 눈금 값 없음", + "add-tick-value": "눈금 값 추가", + "gauge-appearance": "게이지 외관", + "units-title": "단위 제목", + "value": "값", + "ticks": "눈금", + "arrow-and-scale-color": "화살표 및 눈금 기본 색상", + "scale-settings": "눈금 설정", + "scale": "눈금", + "scale-color": "눈금 색상", + "compass-appearance": "나침반 외관", + "label": "라벨", + "labels": "레이블", + "label-style": "레이블 스타일", + "simple-gauge-type": "유형", + "gauge-bar-background": "게이지 막대 배경", + "bar-color": "막대 색상", + "min-and-max-value": "최소 및 최대 값", + "min-and-max-label": "최소 및 최대 레이블", + "font": "글꼴", + "tick-width-and-color": "눈금 너비 및 색상", + "min-max-validation-text": "최대 값은 최소 값보다 커야 합니다." + }, + "gpio": { + "pin": "핀", + "label": "라벨", + "row": "행", + "column": "열", + "color": "색상", + "panel-settings": "패널 설정", + "background-color": "배경색", + "gpio-switches": "GPIO 스위치", + "no-gpio-switches": "구성된 GPIO 스위치 없음", + "add-gpio-switch": "GPIO 스위치 추가", + "gpio-status-request": "GPIO 상태 요청", + "method-name": "메서드 이름", + "method-body": "메서드 본문", + "gpio-status-change-request": "GPIO 상태 변경 요청", + "parse-gpio-status-function": "GPIO 상태 파싱 함수", + "gpio-leds": "GPIO LED", + "no-gpio-leds": "구성된 GPIO LED 없음", + "add-gpio-led": "GPIO LED 추가" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, "input-widgets": { "attribute-not-allowed": "이 위젯에서 속성 파라미터를 사용할 수 없습니다", "blocked-location": "당신의 브라우저에서 위치 정보가 차단되었습니다", @@ -2332,20 +8443,20 @@ "device-name": "장치 이름", "device-name-required": "장치 이름을 입력하세요", "discard-changes": "변경 취소", - "entity-attribute-required": "개체 소성을 입력하세요", + "entity-attribute-required": "엔티티 소성을 입력하세요", "entity-coordinate-required": "위도와 경도를 입력하세요", - "entity-timeseries-required": "개체 시계열을 입력하세요", + "entity-timeseries-required": "엔티티 시계열을 입력하세요", "get-location": "현재 위치 얻기", "invalid-date": "잘못된 날짜", "latitude": "위도", "longitude": "경도", "min-value-error": "최솟값은 {{value}}입니다", "max-value-error": "최댓값은{{value}}입니다", - "not-allowed-entity": "선택된 개체는 속성을 공유할 수 없습니다", + "not-allowed-entity": "선택된 엔티티는 속성을 공유할 수 없습니다", "no-attribute-selected": "아무 속성도 선택되지 않았습니다", "no-datakey-selected": "아무 데이터 키도 선택되지 않았습니다", "no-coordinate-specified": "위도/경도를 위한 데이터가 특정되지 않았습니다", - "no-entity-selected": "아무 개체도 선택되지 않았습니다", + "no-entity-selected": "아무 엔티티도 선택되지 않았습니다", "no-image": "이미지 없음", "no-support-geolocation": "당신의 브라우저는 위치정보를 지원하지 않습니다", "no-support-web-camera": "당신의 브라우저는 카메라를 지원하지 않습니다", @@ -2355,9 +8466,9 @@ "no-timeseries-selected": "아무 시계열도 선택되지 않았습니다", "secret-key": "보안키", "secret-key-required": "보안키를 입력하세요", - "switch-attribute-value": "개체 속성 값으로 전환", + "switch-attribute-value": "엔티티 속성 값으로 전환", "switch-camera": "카메라 전환", - "switch-timeseries-value": "개체 시계열 값으로 전환", + "switch-timeseries-value": "엔티티 시계열 값으로 전환", "take-photo": "사진 촬영", "time": "시간", "timeseries-not-allowed": "이 위젯에서 시계열 파라미터를 사용할 수 없습니다", @@ -2365,29 +8476,1769 @@ "update-successful": "업데이트 성공", "update-attribute": "속성 업데이트", "update-timeseries": "시계열 업데이트", - "value": "값" + "value": "값", + "general-settings": "일반 설정", + "widget-title": "위젯 제목", + "claim-button-label": "등록 버튼 레이블", + "show-secret-key-field": "'비밀 키' 입력 필드 표시", + "labels-settings": "레이블 설정", + "show-labels": "레이블 표시", + "device-name-label": "장치 이름 입력 필드 레이블", + "secret-key-label": "비밀 키 입력 필드 레이블", + "messages-settings": "메시지 설정", + "claim-device-success-message": "장치 등록 성공 메시지", + "claim-device-not-found-message": "장치를 찾을 수 없을 때 메시지", + "claim-device-failed-message": "장치 등록 실패 메시지", + "claim-device-name-required-message": "'장치 이름 필요' 오류 메시지", + "claim-device-secret-key-required-message": "'비밀 키 필요' 오류 메시지", + "show-label": "레이블 표시", + "label": "라벨", + "required": "필수", + "required-error-message": "'필수' 오류 메시지", + "show-result-message": "결과 메시지 표시", + "integer-field-settings": "정수 필드 설정", + "min-value": "최소 값", + "max-value": "최대 값", + "double-field-settings": "실수 필드 설정", + "text-field-settings": "텍스트 필드 설정", + "min-length": "최소 길이", + "max-length": "최대 길이", + "checkbox-settings": "체크박스 설정", + "true-label": "선택됨 레이블", + "false-label": "선택 해제됨 레이블", + "image-input-settings": "이미지 입력 설정", + "display-preview": "미리보기 표시", + "display-clear-button": "지우기 버튼 표시", + "display-apply-button": "적용 버튼 표시", + "display-discard-button": "취소 버튼 표시", + "datetime-field-settings": "날짜/시간 필드 설정", + "display-time-input": "시간 입력 표시", + "latitude-key-name": "위도 키 이름", + "longitude-key-name": "경도 키 이름", + "show-get-location-button": "'현재 위치 가져오기' 버튼 표시", + "use-high-accuracy": "고정밀 사용", + "location-fields-settings": "위치 필드 설정", + "latitude-label": "위도 레이블", + "longitude-label": "경도 레이블", + "input-fields-alignment": "입력 필드 정렬", + "input-fields-alignment-column": "열 (기본값)", + "input-fields-alignment-row": "행", + "layout": "레이아웃", + "row-gap": "행 사이 간격 (픽셀)", + "column-gap": "열 사이 간격 (픽셀)", + "latitude-field-required": "위도 필드 필수", + "longitude-field-required": "경도 필드 필수", + "attribute-settings": "속성 설정", + "widget-mode": "위젯 모드", + "widget-mode-update-attribute": "속성 업데이트", + "widget-mode-update-timeseries": "시계열 업데이트", + "attribute-scope": "속성 범위", + "attribute-scope-server": "서버 속성", + "attribute-scope-shared": "공유된 속성", + "value-required": "값 필요", + "image-settings": "이미지 출력 설정", + "image-format": "이미지 형식", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "이미지 품질", + "max-image-width": "최대 너비", + "max-image-height": "최대 높이", + "action-buttons": "동작 버튼", + "show-action-buttons": "동작 버튼 표시", + "update-all-values": "수정된 것뿐만 아니라 모든 값 업데이트", + "save-button-label": "'저장' 버튼 레이블", + "reset-button-label": "'실행 취소' 버튼 레이블", + "group-settings": "그룹 설정", + "show-group-title": "다른 엔티티와 관련된 필드 그룹의 제목 표시", + "group-title": "그룹 제목", + "fields-alignment": "필드 정렬", + "fields-alignment-row": "행 (기본값)", + "fields-alignment-column": "열", + "fields-in-row": "행의 필드 수", + "option-value": "값 ('null'을 입력하여 빈 옵션 생성)", + "option-label": "라벨", + "hide-input-field": "입력 필드 숨기기", + "datakey-type": "데이터 키 유형", + "datakey-type-server": "서버 속성 (기본값)", + "datakey-type-shared": "공유된 속성", + "datakey-type-timeseries": "시계열", + "datakey-value-type": "데이터 키 값 유형", + "datakey-value-type-string": "문자열", + "datakey-value-type-double": "실수", + "datakey-value-type-integer": "정수", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "불리언 (체크박스)", + "datakey-value-type-boolean-switch": "불리언 (스위치)", + "datakey-value-type-date-time": "날짜 및 시간", + "datakey-value-type-date": "날짜", + "datakey-value-type-time": "시간", + "datakey-value-type-select": "선택", + "datakey-value-type-radio": "라디오", + "datakey-value-type-color": "색상", + "value-is-required": "값은 필수 항목입니다.", + "ability-to-edit-attribute": "속성 편집 가능 여부", + "ability-to-edit-attribute-editable": "편집 가능 (기본값)", + "ability-to-edit-attribute-disabled": "비활성화됨", + "ability-to-edit-attribute-readonly": "읽기 전용", + "disable-on-datakey-name": "다른 데이터 키의 false 값에서 비활성화 (데이터 키 이름 지정)", + "field-appearance": "필드 외관", + "appearance-fill": "채우기", + "appearance-outline": "윤곽선", + "subscript-sizing": "아래 첨자 크기", + "subscript-sizing-fixed": "고정", + "subscript-sizing-dynamic": "동적", + "slide-toggle-settings": "슬라이드 토글 설정", + "slide-toggle-label-position": "슬라이드 토글 레이블 위치", + "slide-toggle-label-position-after": "이후", + "slide-toggle-label-position-before": "앞", + "select-options": "선택 옵션", + "no-select-options": "구성된 선택 옵션 없음", + "add-select-option": "선택 옵션 추가", + "numeric-field-settings": "숫자 필드 설정", + "step-interval": "값 사이의 단계 간격", + "error-messages": "오류 메시지", + "min-value-error-message": "'최소 값' 오류 메시지", + "max-value-error-message": "'최대 값' 오류 메시지", + "invalid-date-error-message": "'잘못된 날짜' 오류 메시지", + "invalid-JSON-error-message": "'잘못된 JSON' 오류 메시지", + "icon-settings": "아이콘 설정", + "dialog-editor-settings": "대화 상자 편집기 설정", + "use-custom-icon": "사용자 지정 아이콘 사용", + "input-cell-icon": "입력 셀 앞에 표시할 아이콘", + "value-conversion-settings": "값 변환 설정", + "get-value-settings": "값 가져오기 설정", + "use-get-value-function": "getValue 함수 사용", + "get-value-function": "getValue 함수", + "set-value-settings": "값 설정 설정", + "use-set-value-function": "setValue 함수 사용", + "set-value-function": "setValue 함수", + "json-invalid": "JSON 값의 형식이 잘못되었습니다", + "title": "제목", + "cancel-button-label": "'취소' 버튼 레이블", + "radio-button-settings": "라디오 버튼 설정", + "color": "색상", + "columns": "열", + "radio-options": "라디오 옵션", + "no-radio-options": "구성된 라디오 옵션 없음", + "add-radio-option": "라디오 옵션 추가", + "radio-label-position": "레이블 위치", + "radio-label-position-before": "앞", + "radio-label-position-after": "이후", + "save-image": "이미지 저장", + "save-to-gallery": "캡처된 이미지를 이미지 갤러리에 자동 저장", + "public-image": "이미지를 승인되지 않은 모든 사용자가 사용할 수 있게 함" + }, + "invalid-qr-code-text": "QR 코드에 대한 잘못된 입력 텍스트입니다. 입력은 문자열 유형이어야 합니다", + "qr-code": { + "use-qr-code-text-function": "QR 코드 텍스트 함수 사용", + "qr-code-text-pattern": "QR 코드 텍스트 패턴 (예: '${entityName} | ${keyName} - 일부 텍스트.')", + "qr-code-text-pattern-hint": "QR 코드 텍스트 패턴은 엔티티 별명의 엔티티에서 처음 찾은 키의 값을 사용합니다.", + "qr-code-text-pattern-required": "QR 코드 텍스트 패턴이 필요합니다.", + "qr-code-text-function": "QR 코드 텍스트 함수" + }, + "label-widget": { + "label-pattern": "패턴", + "label-pattern-hint": "힌트: 예. '텍스트 ${keyName} 단위.' 또는 ${#<키 인덱스>} 단위'", + "label-pattern-required": "패턴이 필요합니다", + "label-position": "위치 (배경에 대한 비율)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "배경색", + "font-settings": "글꼴 설정", + "background-image": "배경 이미지", + "labels": "레이블", + "no-labels": "구성된 레이블 없음", + "add-label": "레이블 추가" + }, + "navigation": { + "title": "제목", + "navigation-path": "탐색 경로", + "filter-type": "필터 유형", + "filter-type-all": "모든 항목", + "filter-type-include": "항목 포함", + "filter-type-exclude": "항목 제외", + "items": "항목", + "enter-urls-to-filter": "필터링할 URL 입력..." + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "메시지 유형", + "method": "방법", + "params": "매개변수", + "created-time": "생성 일시", + "expiration-time": "만료 시간", + "retries": "재시도", + "status": "상태", + "filter": "필터", + "refresh": "새로고침", + "add": "RPC 요청 추가", + "details": "상세 내역", + "delete": "삭제", + "delete-request-title": "영구 RPC 요청 삭제", + "delete-request-text": "요청을 삭제하시겠습니까?", + "details-title": "세부 정보 RPC ID: ", + "additional-info": "추가 정보", + "response": "응답", + "any-status": "모든 상태", + "rpc-status-list": "RPC 상태 목록", + "no-request-prompt": "표시할 요청 없음", + "send-request": "요청 보내기", + "add-title": "영구 RPC 요청 생성", + "method-error": "메서드가 필요합니다.", + "timeout-error": "최소 시간 제한 값은 5000 (5초)입니다.", + "white-space-error": "공백은 허용되지 않습니다.", + "rpc-status": { + "QUEUED": "대기열에 있음", + "SENT": "전송됨", + "DELIVERED": "전달됨", + "SUCCESSFUL": "성공", + "TIMEOUT": "시간 초과", + "EXPIRED": "만료됨", + "FAILED": "실패" + }, + "rpc-search-status-all": "모두", + "message-types": { + "false": "양방향", + "true": "단방향" + }, + "general-settings": "일반 설정", + "enable-filter": "필터 활성화", + "enable-sticky-header": "스크롤하는 동안 헤더 표시", + "enable-sticky-action": "스크롤하는 동안 작업 열 표시", + "display-request-details": "요청 세부 정보 표시", + "allow-send-request": "RPC 요청 보내기 허용", + "allow-delete-request": "요청 삭제 허용", + "columns-settings": "열 설정", + "display-columns": "표시할 열", + "column": "열", + "no-columns-found": "열을 찾을 수 없음", + "no-columns-matching": "'{{column}}'을(를) 찾을 수 없습니다." + }, + "range-chart": { + "chart": "차트", + "data-zoom": "데이터 확대/축소", + "range-chart-appearance": "범위 차트 모양", + "range-colors": "범위 색상", + "out-of-range-color": "범위 외 색상", + "show-range-thresholds": "범위 임계값 표시", + "range-thresholds-settings": "범위 임계값 설정", + "fill-area": "영역 채우기", + "fill-area-opacity": "영역 채우기 불투명도", + "range-chart-style": "범위 차트 스타일" + }, + "knob": { + "behavior": "동작", + "initial-value": "초기 값", + "initial-value-hint": "노브의 초기 값을 가져오는 작업입니다.", + "on-value-change": "값 변경 시", + "on-value-change-hint": "노브 값이 변경될 때 트리거되는 작업입니다.", + "range": "범위", + "min": "최소", + "max": "최대", + "value": "값", + "fallback-initial-value": "대체 초기 값" + }, + "rpc": { + "value-settings": "값 설정", + "initial-value": "초기 값", + "retrieve-value-settings": "켜짐/꺼짐 값 가져오기 설정", + "retrieve-value-method": "메서드를 사용하여 값 가져오기", + "retrieve-value-method-none": "가져오지 않음", + "retrieve-value-method-rpc": "RPC 값 가져오기 메서드 호출", + "retrieve-value-method-attribute": "속성 구독", + "retrieve-value-method-timeseries": "시계열 구독", + "attribute-value-key": "속성 키", + "timeseries-value-key": "시계열 키", + "get-value-method": "RPC 값 가져오기 메서드", + "parse-value-function": "값 구문 분석 함수", + "update-value-settings": "값 업데이트 설정", + "set-value-method": "RPC 값 설정 메서드", + "convert-value-function": "값 변환 함수", + "rpc-settings": "RPC 설정", + "request-timeout": "RPC 요청 시간 제한 (ms)", + "persistent-rpc-settings": "영구 RPC 설정", + "request-persistent": "RPC 요청 영구", + "persistent-polling-interval": "영구 RPC 명령 응답을 가져오기 위한 폴링 간격 (ms)", + "common-settings": "일반 설정", + "switch-title": "스위치 제목", + "show-on-off-labels": "켜짐/꺼짐 레이블 표시", + "slide-toggle-label": "슬라이드 토글 레이블", + "label-position": "레이블 위치", + "label-position-before": "앞", + "label-position-after": "이후", + "slider-color": "슬라이더 색상", + "slider-color-primary": "기본", + "slider-color-accent": "강조", + "slider-color-warn": "경고", + "button-style": "버튼 스타일", + "button-raised": "양각 버튼", + "button-primary": "기본 색상", + "button-background-color": "버튼 배경색", + "button-text-color": "버튼 텍스트 색상", + "widget-title": "위젯 제목", + "button-label": "버튼 레이블", + "device-attribute-scope": "장치 속성 범위", + "server-attribute": "서버 속성", + "shared-attribute": "공유된 속성", + "device-attribute-parameters": "장치 속성 매개변수", + "is-one-way-command": "단방향 명령임", + "rpc-method": "RPC 메서드", + "rpc-method-params": "RPC 메서드 매개변수", + "show-rpc-error": "RPC 명령 실행 오류 표시", + "led-title": "LED 제목", + "led-color": "LED 색상", + "check-status-settings": "상태 확인 설정", + "perform-rpc-status-check": "RPC 장치 상태 확인 수행", + "retrieve-led-status-value-method": "메서드를 사용하여 LED 상태 값 가져오기", + "led-status-value-attribute": "LED 상태 값을 포함하는 장치 속성", + "led-status-value-timeseries": "LED 상태 값을 포함하는 장치 시계열", + "check-status-method": "RPC 장치 상태 확인 메서드", + "parse-led-status-value-function": "LED 상태 값 구문 분석 함수", + "knob-title": "노브 제목" + }, + "maps": { + "map-type": { + "type": "지도 유형", + "map": "지도", + "image": "이미지" + }, + "image": { + "image-source": "이미지 소스", + "image-source-image": "이미지", + "image-source-entity-key": "엔티티 키", + "source-entity-alias": "소스 엔티티 별명", + "image-url-key": "이미지 URL 키", + "image-url-key-required": "이미지 URL 키가 필요합니다" + }, + "control": { + "map-controls": "지도 컨트롤", + "position": "범례 위치", + "position-topleft": "왼쪽 상단", + "position-topright": "오른쪽 상단", + "position-bottomleft": "왼쪽 하단", + "position-bottomright": "오른쪽 하단", + "zoom-actions": "확대/축소 작업", + "zoom-scroll": "스크롤", + "zoom-double-click": "더블 클릭", + "zoom-control-buttons": "컨트롤 버튼", + "scale": "눈금", + "scale-metric": "미터법", + "scale-imperial": "야드파운드법", + "switch-to-drag-mode-using-button": "버튼을 사용하여 드래그 모드로 전환" + }, + "timeline": { + "control-panel": "타임라인 컨트롤 패널", + "time-step": "시간 단계", + "speed-options": "속도 옵션", + "timestamp": "타임스탬프", + "snap-to-real-location": "실제 위치에 맞추기", + "location-snap-filter-function": "위치 맞춤 필터 함수", + "no-trips-data-available": "사용 가능한 이동 데이터 없음" + }, + "map-action": { + "map-action-buttons": "지도 작업 버튼", + "label": "라벨", + "icon": "아이콘", + "color": "색상", + "action": "동작", + "add-button": "버튼 추가", + "no-action-buttons-configured": "구성된 작업 버튼 없음", + "remove-action-button": "작업 버튼 제거", + "map-action-button": "지도 작업 버튼", + "button-requires": "버튼에 레이블 또는 아이콘이 필요합니다" + }, + "common": { + "common-map-settings": "공통 지도 설정", + "fit-map-bounds": "모든 마커를 포함하도록 지도 범위 맞춤", + "default-map-center-position": "기본 지도 중심 위치", + "default-map-zoom-level": "기본 지도 확대/축소 레벨", + "entities-limit": "로드할 엔티티 제한" + }, + "layer": { + "label": "라벨", + "layer": "레이어", + "layers": "레이어", + "map-layers": "지도 레이어", + "add-layer": "레이어 추가", + "layer-settings": "레이어 설정", + "remove-layer": "레이어 제거", + "no-layers": "구성된 레이어 없음", + "roadmap": "도로 지도", + "satellite": "위성", + "hybrid": "하이브리드", + "reference": { + "reference-layer": "참조 레이어", + "no-layer": "레이어 없음", + "openstreetmap-hybrid": "OpenStreetMap 하이브리드", + "world-edition-hybrid": "월드 에디션 하이브리드", + "enhanced-contrast-hybrid": "향상된 대비 하이브리드" + }, + "provider": { + "provider": "제공자", + "openstreet": { + "title": "OpenStreet", + "mapnik": "Mapnik", + "hot": "HOT", + "esri-street": "세계 거리 지도", + "esri-topo": "세계 지형도", + "esri-imagery": "세계 이미지", + "cartodb-positron": "포지트론", + "cartodb-dark-matter": "다크 매터" + }, + "google": { + "title": "Google", + "roadmap": "도로 지도", + "satellite": "위성", + "hybrid": "하이브리드", + "terrain": "지형" + }, + "here": { + "title": "HERE", + "normal-day": "일반 주간", + "normal-night": "일간 야간", + "hybrid-day": "하이브리드 주간", + "terrain-day": "지형 주간" + }, + "tencent": { + "title": "Tencent", + "normal": "정상", + "satellite": "위성", + "terrain": "지형" + }, + "custom": { + "title": "사용자 지정", + "tile-url": "타일 URL" + } + }, + "credentials": { + "credentials": "자격 증명", + "api-key": "API 키" + } + }, + "overlays": { + "overlays": "오버레이", + "overlays-hint": "지도 엔티티에 대한 데이터 소스, 모양, 동작, 편집 옵션 및 그룹화 구성", + "trips": "여행", + "markers": "마커", + "polygons": "다각형", + "circles": "원", + "polylines": "폴리라인" + }, + "data-layer": { + "source": "소스", + "filter": "필터", + "additional-data-keys": "추가 데이터 키", + "additional-datasources": "추가 데이터 소스", + "additional-datasources-hint": "지도에 표시되지 않는 엔티티의 속성 또는 텔레메트리에 액세스하기 위한 데이터 소스, 지도 오버레이 함수에서 사용 가능.", + "more-datasources": "더 많은 데이터 소스", + "data-keys": "데이터 키", + "add-datasource": "데이터소스 추가", + "no-datasources": "구성된 데이터 소스 없음", + "remove-datasource": "데이터소스 삭제", + "behavior": "동작", + "on-click": "클릭 시", + "on-click-hint": "사용자가 지도 항목을 클릭할 때 호출되는 동작.", + "groups": "그룹", + "groups-hint": "오버레이에 할당된 그룹 이름 목록, 지도에서 가시성을 전환하는 데 사용됩니다.", + "color": "색상", + "color-settings": "색상 설정", + "color-type-constant": "상수", + "color-type-range": "범위", + "color-type-function": "함수", + "color-range-source-key": "색상 범위 소스 키", + "color-range-source-key-required": "색상 범위 소스 키는 필수 항목입니다.", + "color-range": "색상 범위", + "color-function": "색상 함수", + "label": "라벨", + "tooltip": "툴팁", + "pattern-type-pattern": "패턴", + "pattern-type-function": "함수", + "label-pattern": "레이블 (패턴 예: '${entityName}', '${entityName}: (텍스트 ${keyName} 단위.)' )", + "label-function": "레이블 함수", + "tooltip-pattern": "툴팁 (예: '텍스트 ${keyName} 단위.' 또는 링크 텍스트)", + "tooltip-function": "툴팁 함수", + "tooltip-trigger": "툴팁 트리거", + "tooltip-trigger-click": "클릭 시 툴팁 표시", + "tooltip-trigger-hover": "호버 시 툴팁 표시", + "auto-close-tooltips": "툴팁 자동 닫기", + "tooltip-offset": "툴팁 오프셋", + "tooltip-offset-horizontal": "가로", + "tooltip-offset-vertical": "세로", + "tooltip-tag-actions": "태그 동작", + "add-tooltip-tag-action": "태그 동작 추가", + "edit-tooltip-tag-action": "태그 동작 편집", + "remove-tooltip-tag-action": "태그 동작 제거", + "action-add": "추가", + "action-edit": "수정", + "action-move": "이동", + "action-remove": "제거", + "edit-instruments": "계기", + "persist-location-attribute-scope": "위치를 저장할 속성의 범위", + "enable-snapping": "정밀 그리기를 위해 다른 정점에 스냅 활성화", + "enable-snapping-hint": "그리기를 더 쉽고 정확하게 만들기 위해 새 포인트를 기존 모양에 자동으로 정렬합니다.", + "drag-drop-mode": "드래그 앤 드롭 모드", + "trip": { + "no-trips": "구성된 여행 없음", + "add-trip": "여행 추가", + "trip-configuration": "여행 구성", + "remove-trip": "여행 제거" + }, + "marker": { + "marker": "마커", + "latitude-key": "위도 키", + "longitude-key": "경도 키", + "x-pos-key": "X 위치 키", + "y-pos-key": "Y 위치 키", + "latitude-key-required": "위도 키 필수", + "longitude-key-required": "경도 키 필수", + "x-pos-key-required": "X 위치 키 필수", + "y-pos-key-required": "Y 위치 키 필수", + "no-markers": "구성된 마커 없음", + "add-marker": "마커 추가", + "marker-configuration": "마커 구성", + "remove-marker": "마커 제거", + "marker-type": "마커 유형", + "marker-type-shape": "모양", + "marker-type-icon": "아이콘", + "marker-type-image": "이미지", + "shape": "모양", + "icon": "아이콘", + "image": "이미지", + "marker-shapes": "마커 모양", + "marker-icon": "마커 아이콘", + "marker-appearance": "마커 외관", + "marker-image": "마커 이미지", + "marker-image-type-image": "이미지", + "marker-image-type-function": "함수", + "custom-marker-image-size": "사용자 지정 마커 이미지 크기", + "marker-image-function": "마커 이미지 함수", + "marker-images": "마커 이미지", + "marker-offset": "마커 오프셋", + "offset-horizontal": "가로", + "offset-vertical": "세로", + "rotate-marker": "마커 회전", + "offset-angle": "오프셋 각도", + "position-conversion": "위치 변환", + "position-conversion-function": "위치 변환 함수, 각각 0에서 1 사이의 double로 x,y 좌표를 반환해야 합니다", + "clustering": { + "use-map-markers-clustering": "지도 마커 클러스터링 사용", + "zoom-on-cluster-click": "클러스터 클릭 시 확대", + "max-zoom": "마커가 클러스터의 일부가 될 수 있는 최대 확대 수준 (0 - 18)", + "max-radius": "클러스터가 커버할 최대 반경", + "zoom-animation": "확대/축소 시 마커 애니메이션", + "bounds-on-cluster-mouse-over": "클러스터 위에 마우스를 올릴 때 마커 경계", + "spiderfy-max-zoom-level": "최대 확대 수준에서 스파이더파이 (모든 클러스터 마커 보기)", + "load-optimization": "로드 최적화", + "chunked-load": "페이지가 멈추지 않도록 청크를 사용하여 마커 추가", + "lazy-load": "마커 추가에 지연 로드 사용", + "use-cluster-marker-color-function": "클러스터 마커 색상 함수 사용", + "marker-color-function": "마커 색상 함수" + }, + "edit": "마커 편집", + "remove-marker-for": "'{{entityName}}'의 마커 제거", + "place-marker": "마커 배치", + "place-marker-hint": "클릭하여 마커 배치", + "place-marker-hint-with-entity": "클릭하여 '{{entityName}}' 엔티티 배치" + }, + "path": { + "path": "경로", + "path-decorator": "경로 장식", + "decorator-symbol": "장식 기호", + "decorator-symbol-arrow-head": "화살표", + "decorator-symbol-dash": "대시", + "decorator-arrangement": "장식 배열", + "decorator-offset": "시작", + "decorator-end-offset": "끝", + "decorator-repeat": "반복" + }, + "points": { + "points": "지점", + "point-tooltip": "지점 툴팁" + }, + "shape": { + "fill": "채우기", + "fill-type-color": "색상", + "fill-type-stripe": "줄무늬", + "fill-type-image": "이미지", + "color": "색상", + "stripe": "줄무늬", + "image": "이미지", + "stroke": "획", + "fill-image": "채우기 이미지", + "fill-image-type-image": "이미지", + "fill-image-type-function": "함수", + "preserve-aspect-ratio": "가로세로 비율 유지", + "opacity": "불투명도", + "angle": "회전 각도", + "scale": "눈금", + "fill-image-function": "모양 채우기 이미지 함수", + "fill-images": "모양 채우기 이미지", + "stripe-pattern": "줄무늬 패턴", + "first-stripe": "첫 번째 줄무늬", + "second-stripe": "두 번째 줄무늬" + }, + "polygon": { + "polygon-key": "다각형 키", + "polygon-key-required": "다각형 키 필수", + "no-polygons": "구성된 다각형 없음", + "add-polygon": "다각형 추가", + "polygon-configuration": "다각형 구성", + "remove-polygon": "다각형 제거", + "edit": "다각형 편집", + "remove-polygon-for": "'{{entityName}}'의 다각형 제거", + "cut": "다각형 영역 자르기", + "rotate": "다각형 회전", + "draw-rectangle": "사각형 그리기", + "draw-polygon": "다각형 그리기", + "polygon-place-first-point-cut-hint": "클릭하여 첫 번째 지점 배치", + "continue-polygon-cut-hint": "클릭하여 계속 그리기", + "finish-polygon-cut-hint": "첫 번째 마커를 클릭하여 완료 및 저장", + "polygon-place-first-point-hint": "다각형: 클릭하여 첫 번째 지점 배치", + "polygon-place-first-point-hint-with-entity": "'{{entityName}}'의 다각형: 클릭하여 첫 번째 지점 배치", + "continue-polygon-hint": "다각형: 클릭하여 계속 그리기", + "continue-polygon-hint-with-entity": "'{{entityName}}'의 다각형: 클릭하여 계속 그리기", + "finish-polygon-hint": "다각형: 첫 번째 마커를 클릭하여 그리기 완료", + "finish-polygon-hint-with-entity": "'{{entityName}}'의 다각형: 첫 번째 마커를 클릭하여 완료 및 저장", + "rectangle-place-first-point-hint": "사각형: 클릭하여 첫 번째 지점 배치", + "rectangle-place-first-point-hint-with-entity": "'{{entityName}}'의 사각형: 클릭하여 첫 번째 지점 배치", + "finish-rectangle-hint": "사각형: 클릭하여 그리기 완료", + "finish-rectangle-hint-with-entity": "'{{entityName}}'의 사각형: 클릭하여 완료 및 저장" + }, + "circle": { + "circle-key": "원 키", + "circle-key-required": "원 키 필수", + "no-circles": "구성된 원 없음", + "add-circle": "원 추가", + "circle-configuration": "원 구성", + "remove-circle": "원 제거", + "edit": "원 편집", + "remove-circle-for": "'{{entityName}}'의 원 제거", + "draw-circle": "원 그리기", + "place-circle-center-hint-with-entity": "'{{entityName}}'의 원: 클릭하여 원 중심 배치", + "place-circle-center-hint": "원: 클릭하여 원 중심 배치", + "finish-circle-hint-with-entity": "'{{entityName}}'의 원: 클릭하여 원 완료 및 저장", + "finish-circle-hint": "원: 클릭하여 그리기 완료" + }, + "polyline": { + "polyline-key": "폴리라인 키", + "polyline-key-required": "폴리라인 키 필수", + "no-polylines": "구성된 폴리라인 없음", + "add-polylines": "폴리라인 추가", + "polyline-configuration": "폴리라인 구성", + "remove-polyline": "폴리라인 제거", + "edit": "폴리라인 편집", + "cut": "폴리라인 영역 자르기", + "rotate": "폴리라인 회전", + "remove-polyline-for": "'{{entityName}}'의 폴리라인 제거", + "draw-polyline": "폴리라인 그리기", + "polyline-place-first-point-hint-with-entity": "'{{entityName}}'의 폴리라인: 클릭하여 첫 번째 지점 배치", + "polyline-place-first-point-hint": "폴리라인: 클릭하여 첫 번째 지점 배치", + "finish-polyline-hint-with-entity": "'{{entityName}}'의 폴리라인: 클릭하여 그리기 완료", + "finish-polyline-hint": "폴리라인: 클릭하여 그리기 완료", + "polyline-place-first-point-cut-hint": "클릭하여 첫 번째 지점 배치", + "finish-polyline-cut-hint": "첫 번째 마커를 클릭하여 완료 및 저장" + }, + "select-entity": "엔티티 선택", + "select-entity-hint": "힌트: 선택 후 지도를 클릭하여 위치 설정" + }, + "select-entity": "엔티티 선택", + "select-entity-hint": "힌트: 선택 후 지도를 클릭하여 위치 설정", + "tooltips": { + "placeMarker": "클릭하여 '{{entityName}}' 엔티티 배치", + "firstVertex": "'{{entityName}}'의 다각형: 클릭하여 첫 번째 지점 배치", + "firstVertex-cut": "클릭하여 첫 번째 지점 배치", + "continueLine": "'{{entityName}}'의 다각형: 클릭하여 계속 그리기", + "continueLine-cut": "클릭하여 계속 그리기", + "finishLine": "완료하려면 기존 마커 클릭", + "finishPoly": "'{{entityName}}'의 다각형: 첫 번째 마커를 클릭하여 완료 및 저장", + "finishPoly-cut": "첫 번째 마커를 클릭하여 완료 및 저장", + "finishRect": "'{{entityName}}'의 다각형: 클릭하여 완료 및 저장", + "startCircle": "'{{entityName}}'의 원: 클릭하여 원 중심 배치", + "finishCircle": "'{{entityName}}'의 원: 클릭하여 원 완료", + "placeCircleMarker": "클릭하여 원 마커 배치" + }, + "actions": { + "finish": "완료", + "cancel": "취소", + "removeLastVertex": "마지막 지점 제거" + }, + "buttonTitles": { + "drawMarkerButton": "엔티티 배치", + "drawPolyButton": "다각형 생성", + "drawLineButton": "폴리라인 생성", + "drawCircleButton": "원 생성", + "drawRectButton": "사각형 생성", + "editButton": "수정 모드", + "dragButton": "드래그 앤 드롭 모드", + "cutButton": "다각형 영역 자르기", + "deleteButton": "제거", + "drawCircleMarkerButton": "원 마커 생성", + "rotateButton": "다각형 회전" + }, + "map-provider-settings": "지도 제공자 설정", + "map-provider": "지도 제공자", + "map-provider-google": "Google 지도", + "map-provider-openstreet": "OpenStreet 지도", + "map-provider-here": "HERE 지도", + "map-provider-image": "이미지 지도", + "map-provider-tencent": "Tencent 지도", + "openstreet-provider": "OpenStreet 지도 공급자", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (기본)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "사용자 지정 공급자 사용", + "custom-provider-tile-url": "사용자 지정 공급자 타일 URL", + "google-maps-api-key": "Google 지도 API 키", + "default-map-type": "기본 지도 유형", + "google-map-type-roadmap": "도로 지도", + "google-map-type-satelite": "위성", + "google-map-type-hybrid": "하이브리드", + "google-map-type-terrain": "지형", + "map-layer": "지도 레이어", + "here-map-normal-day": "HERE.normalDay (기본)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "자격 증명", + "here-app-id": "HERE 앱 ID", + "here-app-code": "HERE 앱 코드", + "here-api-key": "HERE API 키", + "here-use-new-version-api-3": "API 버전 3 사용", + "tencent-maps-api-key": "Tencent 지도 API 키", + "tencent-map-type-roadmap": "도로 지도", + "tencent-map-type-satelite": "위성", + "tencent-map-type-hybrid": "하이브리드", + "image-map-background": "이미지 지도 배경", + "image-map-background-from-entity-attribute": "엔티티 속성에서 이미지 지도 배경 가져오기", + "image-url-source-entity-alias": "이미지 URL 소스 엔티티 별명", + "image-url-source-entity-attribute": "이미지 URL 소스 엔티티 속성", + "common-map-settings": "공통 지도 설정", + "x-pos-key-name": "X 위치 키 이름", + "y-pos-key-name": "Y 위치 키 이름", + "latitude-key-name": "위도 키 이름", + "longitude-key-name": "경도 키 이름", + "default-map-zoom-level": "기본 지도 확대/축소 레벨 (0 - 20)", + "default-map-center-position": "기본 지도 중심 위치 (0,0)", + "disable-scroll-zooming": "스크롤 확대/축소 비활성화", + "disable-double-click-zooming": "더블 클릭 확대/축소 비활성화", + "disable-zoom-control-buttons": "확대/축소 컨트롤 버튼 비활성화", + "fit-map-bounds": "모든 마커를 포함하도록 지도 범위 맞춤", + "use-default-map-center-position": "기본 지도 중심 위치 사용", + "entities-limit": "로드할 엔티티 제한", + "markers-settings": "마커 설정", + "marker-offset-x": "마커 너비를 곱한 위치에 대한 마커 X 오프셋", + "marker-offset-y": "마커 높이를 곱한 위치에 대한 마커 Y 오프셋", + "position-function": "위치 변환 함수, 각각 0에서 1 사이의 double로 x,y 좌표를 반환해야 합니다", + "draggable-marker": "드래그 가능한 마커", + "label": "라벨", + "show-label": "레이블 표시", + "use-label-function": "레이블 함수 사용", + "label-pattern": "레이블 (패턴 예: '${entityName}', '${entityName}: (텍스트 ${keyName} 단위.)' )", + "label-function": "레이블 함수", + "tooltip": "툴팁", + "show-tooltip": "툴팁 표시", + "show-tooltip-action": "툴팁 표시 작업", + "show-tooltip-action-click": "클릭 시 툴팁 표시 (기본)", + "show-tooltip-action-hover": "호버 시 툴팁 표시", + "auto-close-tooltips": "툴팁 자동 닫기", + "use-tooltip-function": "툴팁 함수 사용", + "tooltip-pattern": "툴팁 (예: '텍스트 ${keyName} 단위.' 또는 링크 텍스트')", + "tooltip-function": "툴팁 함수", + "tooltip-offset-x": "마커 너비를 곱한 마커 앵커에 대한 툴팁 X 오프셋", + "tooltip-offset-y": "마커 높이를 곱한 마커 앵커에 대한 툴팁 Y 오프셋", + "color": "색상", + "use-color-function": "색상 함수 사용", + "color-function": "색상 함수", + "marker-image": "마커 이미지", + "use-marker-image-function": "마커 이미지 함수 사용", + "custom-marker-image": "사용자 지정 마커 이미지", + "custom-marker-image-size": "사용자 지정 마커 이미지 크기 (px)", + "marker-image-function": "마커 이미지 함수", + "marker-images": "마커 이미지", + "polygon-settings": "다각형 설정", + "show-polygon": "다각형 표시", + "polygon-key-name": "다각형 키 이름", + "enable-polygon-edit": "다각형 편집 활성화", + "polygon-label": "다각형 레이블", + "show-polygon-label": "다각형 레이블 표시", + "use-polygon-label-function": "다각형 레이블 함수 사용", + "polygon-label-pattern": "다각형 레이블 (패턴 예: '${entityName}', '${entityName}: (텍스트 ${keyName} 단위.)' )", + "polygon-label-function": "다각형 레이블 함수", + "polygon-tooltip": "다각형 툴팁", + "show-polygon-tooltip": "다각형 툴팁 표시", + "auto-close-polygon-tooltips": "다각형 툴팁 자동 닫기", + "use-polygon-tooltip-function": "다각형 툴팁 함수 사용", + "polygon-tooltip-pattern": "툴팁 (예: '텍스트 ${keyName} 단위.' 또는 링크 텍스트')", + "polygon-tooltip-function": "다각형 툴팁 함수", + "polygon-color": "다각형 색상", + "polygon-opacity": "다각형 불투명도", + "use-polygon-color-function": "다각형 색상 함수 사용", + "polygon-color-function": "다각형 색상 함수", + "polygon-stroke": "다각형 획", + "stroke-color": "획 색상", + "stroke-opacity": "획 불투명도", + "stroke-weight": "획 두께", + "use-polygon-stroke-color-function": "다각형 획 색상 함수 사용", + "polygon-stroke-color-function": "다각형 획 색상 함수", + "circle-settings": "원 설정", + "show-circle": "원 표시", + "circle-key-name": "원 키 이름", + "enable-circle-edit": "원 편집 활성화", + "circle-label": "원 레이블", + "show-circle-label": "원 레이블 표시", + "use-circle-label-function": "원 레이블 함수 사용", + "circle-label-pattern": "원 레이블 (패턴 예: '${entityName}', '${entityName}: (텍스트 ${keyName} 단위.)' )", + "circle-label-function": "원 레이블 함수", + "circle-tooltip": "원 툴팁", + "show-circle-tooltip": "원 툴팁 표시", + "auto-close-circle-tooltips": "원 툴팁 자동 닫기", + "use-circle-tooltip-function": "원 툴팁 함수 사용", + "circle-tooltip-pattern": "툴팁 (예: '텍스트 ${keyName} 단위.' 또는 링크 텍스트')", + "circle-tooltip-function": "원 툴팁 함수", + "circle-fill-color": "원 채우기 색상", + "circle-fill-color-opacity": "원 채우기 색상 불투명도", + "use-circle-fill-color-function": "원 채우기 색상 함수 사용", + "circle-fill-color-function": "원 채우기 색상 함수", + "circle-stroke": "원 획", + "use-circle-stroke-color-function": "원 획 색상 함수 사용", + "circle-stroke-color-function": "원 획 색상 함수", + "markers-clustering-settings": "마커 클러스터링 설정", + "use-map-markers-clustering": "지도 마커 클러스터링 사용", + "zoom-on-cluster-click": "클러스터 클릭 시 확대", + "max-cluster-zoom": "마커가 클러스터의 일부가 될 수 있는 최대 확대 수준 (0 - 18)", + "max-cluster-radius-pixels": "클러스터가 덮을 최대 반지름 (픽셀)", + "cluster-zoom-animation": "확대/축소 시 마커에 애니메이션 표시", + "show-markers-bounds-on-cluster-mouse-over": "클러스터 위에 마우스를 올릴 때 마커 범위 표시", + "spiderfy-max-zoom-level": "최대 확대 수준에서 스파이더파이 (모든 클러스터 마커 보기)", + "load-optimization": "로드 최적화", + "cluster-chunked-loading": "페이지가 멈추지 않도록 청크를 사용하여 마커 추가", + "cluster-markers-lazy-load": "마커 추가에 지연 로드 사용", + "editor-settings": "편집기 설정", + "enable-snapping": "정밀 그리기를 위해 다른 정점에 스냅 활성화", + "init-draggable-mode": "드래그 모드로 지도 초기화", + "hide-all-edit-buttons": "모든 편집 컨트롤 버튼 숨기기", + "hide-draw-buttons": "그리기 버튼 숨기기", + "hide-edit-buttons": "편집 버튼 숨기기", + "hide-remove-button": "제거 버튼 숨기기", + "route-map-settings": "경로 지도 설정", + "trip-animation-settings": "이동 애니메이션 설정", + "normalization-step": "정규화 데이터 단계 (ms)", + "tooltip-background-color": "툴팁 배경색", + "tooltip-font-color": "툴팁 글꼴 색상", + "tooltip-opacity": "툴팁 불투명도 (0-1)", + "auto-close-tooltip": "툴팁 자동 닫기", + "rotation-angle": "마커에 대한 추가 회전 각도 설정 (도)", + "path-settings": "경로 설정", + "path-color": "경로 색상", + "use-path-color-function": "경로 색상 함수 사용", + "path-color-function": "경로 색상 함수", + "path-decorator": "경로 장식", + "use-path-decorator": "경로 데코레이터 사용", + "decorator-symbol": "장식 기호", + "decorator-symbol-arrow-head": "화살표", + "decorator-symbol-dash": "대시", + "decorator-symbol-size": "데코레이터 기호 크기 (px)", + "use-path-decorator-custom-color": "경로 데코레이터 사용자 지정 색상 사용", + "decorator-custom-color": "데코레이터 사용자 지정 색상", + "decorator-offset": "데코레이터 오프셋", + "end-decorator-offset": "종료 데코레이터 오프셋", + "decorator-repeat": "데코레이터 반복", + "points-settings": "포인트 설정", + "show-points": "지점 표시", + "point-color": "포인트 색상", + "point-size": "포인트 크기 (px)", + "use-point-color-function": "포인트 색상 함수 사용", + "point-color-function": "포인트 색상 함수", + "use-point-as-anchor": "포인트를 앵커로 사용", + "point-as-anchor-function": "포인트를 앵커로 사용 함수", + "independent-point-tooltip": "독립 포인트 툴팁", + "clustering-markers": "클러스터링 마커", + "use-icon-create-function": "마커 색상 함수 사용", + "marker-color-function": "마커 색상 함수" + }, + "markdown": { + "use-markdown-text-function": "마크다운/HTML 값 함수 사용", + "markdown-text-function": "마크다운/HTML 값 함수", + "markdown-text-pattern": "마크다운/HTML 패턴 (변수가 있는 마크다운 또는 HTML, 예: '${entityName} 또는 ${keyName} - 일부 텍스트.')", + "apply-default-markdown-style": "기본 마크다운 스타일 적용", + "markdown-css": "마크다운/HTML CSS" + }, + "simple-card": { + "label": "라벨", + "label-position": "레이블 위치", + "label-position-left": "왼쪽", + "label-position-top": "상단" + }, + "single-switch": { + "behavior": "동작", + "layout": "레이아웃", + "layout-right": "오른쪽", + "layout-left": "왼쪽", + "layout-centered": "중앙 정렬", + "auto-scale": "자동 크기 조정", + "label": "라벨", + "icon": "아이콘", + "switch-color": "스위치 색상", + "on": "켜기", + "off": "끄기", + "disabled": "비활성화됨", + "tumbler-color": "텀블러 색상", + "on-label": "켜짐 레이블", + "off-label": "꺼짐 레이블", + "switch": "스위치" + }, + "slider": { + "behavior": "동작", + "initial-value": "초기 값", + "initial-value-hint": "슬라이더의 초기 값을 가져오는 작업입니다.", + "on-value-change": "값 변경 시", + "on-value-change-hint": "슬라이더 값이 변경될 때 트리거되는 작업입니다.", + "layout": "레이아웃", + "layout-default": "기본값", + "layout-extended": "확장", + "layout-simplified": "간소화", + "auto-scale": "자동 크기 조정", + "icon": "아이콘", + "value": "값", + "range": "범위", + "min": "최소", + "max": "최대", + "range-ticks": "범위 눈금", + "tick-marks": "눈금 표시", + "colors": "색상", + "main": "메인", + "background": "배경", + "left-icon": "왼쪽 아이콘", + "right-icon": "오른쪽 아이콘", + "slider": "슬라이더" + }, + "value-card": { + "layout": "레이아웃", + "layout-square": "사각형", + "layout-vertical": "세로", + "layout-centered": "중앙 정렬", + "layout-simplified": "간소화", + "layout-horizontal": "가로", + "layout-horizontal-reversed": "수평 반전", + "label": "라벨", + "icon": "아이콘", + "value": "값", + "date": "날짜", + "value-card-style": "값 카드 스타일", + "auto-scale": "자동 크기 조정" + }, + "label-card": { + "auto-scale": "자동 크기 조정", + "label": "라벨", + "icon": "아이콘", + "label-card-style": "레이블 카드 스타일" + }, + "label-value-card": { + "value": "값", + "label-value-card-style": "레이블 및 값 카드 스타일" + }, + "liquid-level-card": { + "layout-simple": "간단", + "layout-percentage": "백분율", + "layout-absolute": "절대값", + "layout": "레이아웃", + "background-overlay": "값 배경 오버레이", + "total-volume": "총 볼륨", + "total-volume-units": "총 볼륨 단위", + "tank": "탱크", + "shape": "모양", + "datasource-units": "소스 단위", + "widget-units": "위젯 단위", + "decimals": "소수 자릿수", + "liquid": "액체", + "liquid-color": "액체 색상", + "value": "값", + "value-font": "값 글꼴", + "level": "수준", + "last-update": "마지막 업데이트", + "shape-by-attribute": "속성 이름으로 탱크 모양 설정", + "tooltip-background": "배경색", + "background-blur": "배경 블러", + "tank-color": "탱크 색상", + "static": "정적", + "see-examples": "예시 보기", + "attribute": "속성", + "shape-type": "유형", + "v-oval": "수직 타원", + "v-cylinder": "수직 원통", + "v-capsule": "수직 캡슐", + "rectangle": "사각형", + "h-oval": "수평 타원", + "h-ellipse": "수평 타원", + "h-dish-ends": "수평 접시 끝", + "h-cylinder": "수평 원통", + "h-capsule": "수평 캡슐", + "h-elliptical_2_1": "수평 2:1 타원", + "icon": "카드 아이콘", + "title": "카드 제목", + "units": "단위", + "color-and-font": "색상 및 글꼴", + "shape-attribute-name": "속성 이름", + "total-volume-required": "총 볼륨이 필요합니다.", + "attribute-name-required": "속성 이름이 필요합니다.", + "attribute-key-not-set": "속성 '{{attributeName}}' 키가 설정되지 않음", + "attribute-key-invalid": "속성 '{{attributeName}}' 키가 유효하지 않음" + }, + "aggregated-value-card": { + "subtitle": "부제목", + "chart": "차트", + "values": "값", + "value-appearance": "값 외관", + "position": "범례 위치", + "position-center": "중앙", + "position-right-top": "오른쪽 상단", + "position-right-bottom": "오른쪽 하단", + "position-left-top": "왼쪽 상단", + "position-left-bottom": "왼쪽 하단", + "font": "글꼴", + "color": "색상", + "arrow": "화살표", + "display-up-down-arrow": "위/아래 화살표 표시", + "add-value": "값 추가", + "remove-value": "값 제거", + "no-values": "구성된 값 없음", + "aggregation": "집합", + "aggregated-value-card-style": "집계 값 카드 스타일", + "auto-scale": "자동 크기 조정" + }, + "value-chart-card": { + "layout": "레이아웃", + "layout-left": "왼쪽", + "layout-right": "오른쪽", + "auto-scale": "자동 크기 조정", + "icon": "아이콘", + "value": "값", + "chart": "차트", + "value-chart-card-style": "값 차트 카드 스타일" + }, + "progress-bar": { + "layout": "레이아웃", + "layout-default": "기본값", + "layout-simplified": "간소화", + "auto-scale": "자동 크기 조정", + "icon": "아이콘", + "value": "값", + "range": "범위", + "min": "최소", + "max": "최대", + "range-ticks": "범위 눈금", + "bar": "막대", + "bar-color": "막대 색상", + "bar-background": "막대 배경", + "progress-bar-card-style": "진행 막대 카드 스타일" + }, + "notification": { + "max-notification-display": "표시할 최대 알림 수", + "counter": "카운터", + "counter-hint": "\"위젯 제목\"이 활성화되면 카운터가 표시됩니다.", + "icon": "아이콘", + "counter-value": "값", + "counter-color": "색상", + "notification-button": "알림 버튼", + "button-view-all": "모두 보기", + "button-filter": "필터", + "type-filter": "유형 필터", + "button-mark-read": "모두 읽음으로 표시", + "notification-types": "알림 유형", + "notification-type": "알림 유형", + "search-type": "유형 검색", + "any-type": "모든 유형" + }, + "alarm-count": { + "alarm-count-card-style": "알람 수 카드 스타일" + }, + "entity-count": { + "entity-count-card-style": "엔티티 수 카드 스타일" + }, + "count": { + "layout": "레이아웃", + "layout-column": "열", + "layout-row": "행", + "label": "라벨", + "icon": "아이콘", + "icon-background": "아이콘 배경", + "value": "값", + "chevron": "갈매기", + "auto-scale": "자동 크기 조정" + }, + "table": { + "common-table-settings": "일반 테이블 설정", + "enable-search": "검색 활성화", + "enable-sticky-header": "항상 헤더 표시", + "enable-sticky-action": "항상 동작 열 표시", + "hidden-cell-button-display-mode": "숨겨진 셀 버튼 동작 표시 모드", + "show-empty-space-hidden-action": "숨겨진 셀 버튼 동작 대신 빈 공간 표시", + "dont-reserve-space-hidden-action": "숨겨진 동작 버튼에 공간 예약 안 함", + "display-timestamp": "타임스탬프", + "timestamp-column-name": "타임스탬프", + "display-pagination": "페이지 매김 표시", + "default-page-size": "기본 페이지 크기", + "page-step-settings": "페이지 단계 설정", + "page-step-count": "단계 수", + "page-step-increment": "단계 증가", + "page-step-count-format-message": "1에서 100 범위의 정수 값이어야 합니다.", + "page-step-increment-format-message": "1 이상의 정수 값이어야 합니다.", + "use-entity-label-tab-name": "탭 이름에 엔티티 레이블 사용", + "hide-empty-lines": "빈 줄 숨기기", + "row-style": "행 스타일", + "use-row-style-function": "행 스타일 함수 사용", + "row-style-function": "행 스타일 함수", + "cell-style": "셀 스타일", + "use-cell-style-function": "셀 스타일 함수 사용", + "cell-style-function": "셀 스타일 함수", + "cell-content": "셀 내용", + "use-cell-content-function": "셀 내용 함수 사용", + "cell-content-function": "셀 내용 함수", + "show-latest-data-column": "최신 데이터 열 표시", + "latest-data-column-order": "최신 데이터 열 순서", + "entities-table-title": "엔티티 테이블 제목", + "enable-select-column-display": "표시할 열 선택 활성화", + "display-entity-name": "엔티티 이름 열 표시", + "entity-name-column-title": "엔티티 이름 열 제목", + "display-entity-label": "엔티티 레이블 열 표시", + "entity-label-column-title": "엔티티 레이블 열 제목", + "display-entity-type": "엔티티 유형 열 표시", + "default-sort-order": "기본 정렬 순서", + "custom-title": "사용자 지정 헤더 제목", + "column-width": "열 너비 (px 또는 %)", + "default-column-visibility": "기본 열 가시성", + "column-visibility-visible": "표시", + "column-visibility-hidden": "숨김", + "column-visibility-hidden-mobile": "모바일 모드에서 숨김", + "column-selection-to-display": "'표시할 열'에서 열 선택", + "column-selection-to-display-enabled": "사용", + "column-selection-to-display-disabled": "비활성화됨", + "alarms-table-title": "알람 테이블 제목", + "enable-alarms-selection": "알람 선택 활성화", + "enable-alarms-search": "알람 검색 활성화", + "enable-alarm-filter": "알람 필터 활성화", + "display-alarm-details": "알람 세부 정보 표시", + "allow-alarms-ack": "알람 확인 허용", + "allow-alarms-clear": "알람 지우기 허용", + "display-alarm-activity": "알람 활동 표시", + "allow-alarms-assign": "알람 할당 허용", + "columns": "열", + "column-settings": "열 설정", + "remove-column": "열 제거", + "add-column": "열 추가", + "no-columns": "구성된 열 없음", + "columns-to-display": "표시할 열", + "table-header": "테이블 헤더", + "header-buttons": "헤더 버튼", + "table-buttons": "테이블 버튼", + "pagination": "페이지 매김", + "rows": "행", + "timeseries-column-error": "최소 하나의 시계열 열을 지정해야 합니다.", + "alarm-column-error": "최소 하나의 알람 열을 지정해야 합니다.", + "table-tabs": "테이블 탭", + "show-cell-actions-menu-mobile": "모바일 모드에서 셀 동작 드롭다운 메뉴 표시", + "disable-sorting": "정렬 비활성화", + "sort-by": "탭 정렬 기준", + "sort-timestamp-option": "생성 일시" + }, + "latest-chart": { + "total": "합계", + "auto-scale": "자동 크기 조정", + "clockwise-layout": "시계 방향 레이아웃", + "sort-series": "레이블로 시리즈 정렬", + "tooltip-value-type-absolute": "절대값", + "tooltip-value-type-percentage": "백분율" + }, + "pie-chart": { + "pie-chart-appearance": "원형 차트 외관", + "label": "라벨", + "border": "테두리", + "radius": "반경", + "pie-chart-card-style": "원형 차트 카드 스타일" + }, + "radar-chart": { + "radar-appearance": "레이더 외관", + "shape": "모양", + "shape-polygon": "다각형", + "shape-circle": "원", + "color": "색상", + "line": "선", + "points": "지점", + "points-label": "지점 레이블", + "radar-axis": "레이더 축", + "axis-label": "축 레이블", + "ticks-label": "눈금 레이블", + "radar-chart-style": "레이더 차트 스타일", + "max-axes-scaling": "최대 축 크기 조정", + "max-axes-scaling-hint": "각 레이더 축이 자체 최대값(별도)을 갖는지 또는 위젯 데이터 세트를 기반으로 모든 축에서 가장 높은 값(공통)을 공유하는지 선택합니다.", + "separate": "분리", + "common": "공통" + }, + "time-series-chart": { + "chart": "차트", + "chart-style": "차트 스타일", + "data-zoom": "데이터 확대/축소", + "stack-mode": "스택 모드", + "stack-mode-hint": "차트에 시리즈를 스택합니다. 동일한 단위의 시리즈는 서로 위에 배치됩니다.", + "axes": "축", + "y-axes": "Y축", + "line-type": "선 유형", + "line-width": "선 너비", + "type-line": "선", + "type-bar": "막대", + "type-point": "지점", + "no-aggregation-bar-width-strategy": "비집계 데이터의 막대 너비 전략", + "no-aggregation-bar-width-strategy-group": "그룹", + "no-aggregation-bar-width-strategy-separate": "분리", + "bar-group-width": "막대 그룹 너비", + "bar-width": "막대 너비", + "bar-width-relative": "시간 창의 백분율", + "bar-width-absolute": "절대값 (ms)", + "comparison": { + "comparison": "비교", + "comparison-hint": "비교는 기록 데이터에서만 작동합니다!", + "show": "표시", + "settings": "비교 설정", + "show-values-for-comparison": "비교를 위한 기록 데이터 표시", + "comparison-values-label": "비교 키 레이블", + "comparison-values-label-auto": "자동", + "comparison-data-color": "비교 데이터 색상" + }, + "threshold": { + "thresholds": "임계값", + "source": "소스", + "key-value": "키 / 값", + "no-thresholds": "구성된 임계값 없음", + "add-threshold": "임계값 추가", + "type-constant": "상수", + "type-latest-key": "키", + "type-entity": "엔티티", + "threshold-settings": "임계값 설정", + "remove-threshold": "임계값 제거", + "threshold-value-required": "임계값은 필수 항목입니다.", + "key-required": "키는 필수 항목입니다.", + "entity-key-required": "엔티티 키는 필수 항목입니다.", + "line-appearance": "선 외관", + "line-color": "선 색상", + "start-symbol": "시작 기호", + "end-symbol": "끝 기호", + "symbol-size": "크기", + "label": "라벨", + "label-position-start": "시작", + "label-position-middle": "중간", + "label-position-end": "끝", + "label-position-inside-start": "내부 시작", + "label-position-inside-start-top": "내부 시작 상단", + "label-position-inside-start-bottom": "내부 시작 하단", + "label-position-inside-middle": "내부 중간", + "label-position-inside-middle-top": "내부 중간 상단", + "label-position-inside-middle-bottom": "내부 중간 하단", + "label-position-inside-end": "내부 끝", + "label-position-inside-end-top": "내부 끝 상단", + "label-position-inside-end-bottom": "내부 끝 하단", + "label-background": "레이블 배경" + }, + "state": { + "states": "상태", + "label": "라벨", + "ticks-value": "눈금 값", + "source": "소스", + "value-range": "값 / 범위", + "no-states": "구성된 상태 없음", + "add-state": "상태 추가", + "type-constant": "상수", + "type-range": "범위", + "from": "부터", + "to": "까지", + "remove-state": "상태 제거" + }, + "grid": { + "grid": "그리드", + "background-color": "배경색", + "border": "테두리" + }, + "axis": { + "axes": "축", + "x-axis": "X축", + "y-axis": "Y축", + "y-axis-settings": "Y축 설정", + "comparison-x-axis-settings": "비교 X축 설정", + "remove-y-axis": "Y축 제거", + "id": "ID", + "label": "라벨", + "position": "범례 위치", + "position-left": "왼쪽", + "position-right": "오른쪽", + "position-top": "상단", + "position-bottom": "하단", + "tick-labels": "눈금 레이블", + "ticks-formatter-function": "눈금 형식 함수", + "ticks-generator-function": "눈금 생성기 함수", + "show-ticks": "눈금 표시", + "show-line": "선 표시", + "show-split-lines": "분할선 표시", + "show-split-lines-x-axis-hint": "활성화하면 차트에 수직선이 표시됩니다.", + "show-split-lines-y-axis-hint": "활성화하면 차트에 수평선이 표시됩니다.", + "ticks-interval": "눈금 간격", + "ticks-interval-hint": "축의 분할 간격을 강제로 설정합니다.", + "split-number": "분할 수", + "split-number-hint": "축이 분할되는 세그먼트 수입니다.", + "min": "최소", + "max": "최대", + "show": "표시", + "add-y-axis": "Y축 추가" + }, + "series": { + "legend-settings": "범례 설정", + "show-in-legend": "범례에 표시", + "show-in-legend-hint": "범례에 시리즈 이름과 데이터를 표시합니다.", + "hidden-by-default": "기본적으로 숨김", + "hidden-by-default-hint": "시리즈를 기본적으로 범례에서 숨깁니다.", + "series-type": "시리즈 유형", + "type": "유형", + "type-line": "선", + "type-bar": "막대", + "line": { + "line": "선", + "show-line": "선 표시", + "step-line": "계단선", + "step-type-start": "시작", + "step-type-middle": "중간", + "step-type-end": "끝", + "smooth-line": "부드러운 선" + }, + "point": { + "points": "지점", + "show-points": "지점 표시", + "point-label": "지점 레이블", + "point-label-hint": "시리즈 지점 위에 값이 있는 레이블을 표시합니다.", + "point-label-background": "지점 레이블 배경", + "point-shape": "포인트 모양", + "point-size": "포인트 크기" + } + } + }, + "wind-speed-direction": { + "layout": "레이아웃", + "layout-default": "기본값", + "layout-advanced": "고급", + "layout-simplified": "간소화", + "values": "값", + "wind-direction": "풍향", + "center-value": "중심 값", + "icon": "아이콘", + "arrow": "화살표", + "ticks": "눈금", + "labels-type": "레이블 유형", + "directional-names": "방향 이름", + "degrees": "도", + "major-ticks": "주 눈금", + "minor-ticks": "보조 눈금", + "wind-speed-direction-card-style": "풍속 및 풍향 카드 스타일", + "ticks-color": "눈금 색상", + "ticks-labels-type": "눈금 레이블 유형", + "arrow-color": "화살표 색상" + }, + "value-source": { + "value-source": "값 소스", + "predefined-value": "상수", + "entity-attribute": "엔티티 속성", + "value": "값", + "value-required": "값이 필요합니다.", + "key-required": "키는 필수 항목입니다.", + "entity-key-required": "엔티티 키는 필수 항목입니다.", + "source-entity-alias": "소스 엔티티 별명", + "source-entity-attribute": "소스 엔티티 속성", + "type-constant": "상수", + "type-latest-key": "키", + "type-entity": "엔티티" + }, + "rpc-state": { + "initial-state": "초기 상태", + "initial-state-hint": "컴포넌트의 초기 상태(켜짐/꺼짐)를 가져오는 작업입니다.", + "disabled-state": "비활성화 상태", + "disabled-state-hint": "구성 요소가 비활성화되는 조건을 구성합니다.", + "turn-on": "'켜짐'으로 전환", + "turn-on-hint": "슬라이더가 '켜짐'으로 전환될 때 트리거되는 작업입니다.", + "turn-off": "'꺼짐'으로 전환", + "turn-off-hint": "슬라이더가 '꺼짐'으로 전환될 때 트리거되는 작업입니다.", + "on": "켜기", + "off": "끄기", + "disabled": "비활성화됨" + }, + "value-action": { + "do-nothing": "아무것도 하지 않음", + "execute-rpc": "RPC 실행", + "get-attribute": "속성 가져오기", + "set-attribute": "속성 설정", + "get-time-series": "시계열 가져오기", + "get-alarm-status": "알람 상태 가져오기", + "get-dashboard-state": "대시보드 상태 ID 가져오기", + "get-dashboard-state-object": "대시보드 상태 엔티티 가져오기", + "add-time-series": "시계열 추가", + "execute-rpc-text": "RPC 메서드 '{{methodName}}' 실행", + "get-time-series-text": "시계열 '{{key}}' 사용", + "get-attribute-text": "속성 '{{key}}' 사용", + "get-alarm-status-text": "알람 상태 사용", + "get-dashboard-state-text": "대시보드 상태 사용", + "get-dashboard-state-object-text": "대시보드 상태 엔티티 사용", + "when-dashboard-state-is-text": "대시보드 상태 ID가 '{{state}}'인 경우", + "when-dashboard-state-function-is-text": "f(대시보드 상태 ID)가 '{{state}}'인 경우", + "when-dashboard-state-object-function-is-text": "f(대시보드 상태 엔티티)가 '{{state}}'인 경우", + "set-attribute-to-value-text": "'{{key}}' 속성을 다음으로 설정: {{value}}", + "add-time-series-value-text": "'{{key}}' 시계열 값 추가: {{value}}", + "set-attribute-text": "'{{key}}' 속성 설정", + "add-time-series-text": "'{{key}}' 시계열 추가", + "action": "동작", + "value": "값", + "init-value-hint": "장치가 데이터를 보낼 때까지 설정될 값입니다.", + "method": "방법", + "method-name-required": "메서드 이름이 필요합니다.", + "request-timeout-ms": "RPC 요청 시간 제한 (ms)", + "request-timeout-required": "요청 시간 제한이 필요합니다.", + "min-request-timeout-error": "요청 시간 제한 값은 5000ms (5초) 이상이어야 합니다.", + "request-persistent": "RPC 요청 영구", + "persistent-polling-interval": "영구 폴링 간격 (ms)", + "persistent-polling-interval-hint": "영구 RPC 명령 응답을 가져오기 위한 폴링 간격 (ms)", + "persistent-polling-interval-required": "영구 폴링 간격이 필요합니다.", + "min-persistent-polling-interval-error": "영구 폴링 간격 값은 1000ms (1초) 이상이어야 합니다.", + "attribute-scope": "속성 범위", + "attribute-key": "속성 키", + "attribute-key-required": "속성 키를 입력하세요.", + "time-series-key": "시계열 키", + "time-series-key-required": "시계열 키가 필요합니다.", + "action-result-converter": "작업 결과 변환기", + "converter-none": "없음", + "converter-function": "함수", + "converter-constant": "상수", + "converter-value": "값", + "parse-value-function": "값 구문 분석 함수", + "state-when-result-is": "결과가 다음인 경우 '{{state}}'", + "parameters": "파라미터", + "convert-value-function": "값 변환 함수", + "error": { + "target-entity-is-not-set": "대상 엔티티가 설정되지 않았습니다!", + "failed-to-perform-action": "{{ actionLabel }} 작업을 수행하지 못했습니다.", + "invalid-attribute-scope": "{{scope}} 속성 범위는 {{entityType}} 엔티티에서 지원되지 않습니다." + } + }, + "widget-font": { + "font-settings": "글꼴 설정", + "font-family": "글꼴 패밀리", + "size": "크기", + "relative-font-size": "상대 글꼴 크기 (백분율)", + "font-style": "스타일", + "font-style-normal": "정상", + "font-style-italic": "이탤릭", + "font-style-oblique": "기울임", + "font-weight": "두께", + "font-weight-normal": "정상", + "font-weight-bold": "굵게", + "font-weight-bolder": "더 굵게", + "font-weight-lighter": "더 얇게", + "color": "색상", + "shadow-color": "그림자 색상", + "preview": "미리보기", + "line-height": "줄 높이", + "auto": "자동" + }, + "home": { + "no-data-available": "사용 가능한 데이터 없음" + }, + "system-info": { + "cpu": "CPU", + "ram": "RAM", + "disk": "디스크", + "cpu-warning-text": "CPU 사용량이 높습니다. 시스템 오류를 방지하려면 시스템 성능을 최적화하세요.", + "cpu-critical-text": "CPU 사용량이 매우 높습니다. 시스템 오류를 방지하려면 시스템 성능을 최적화하세요.", + "ram-warning-text": "RAM 여유 공간이 부족합니다. 시스템 오류를 방지하려면 시스템 성능을 최적화하거나 RAM 크기를 늘리세요.", + "ram-critical-text": "RAM 여유 공간이 매우 부족합니다. 시스템 오류를 방지하려면 시스템 성능을 최적화하거나 RAM 크기를 늘리세요.", + "disk-warning-text": "디스크 공간이 부족합니다. 데이터 손실을 방지하려면 디스크 공간을 확보하거나 늘리세요.", + "disk-critical-text": "디스크 공간이 매우 부족합니다. 데이터 손실을 방지하려면 디스크 공간을 확보하거나 늘리세요." + }, + "cluster-info": { + "service-id": "서비스 ID", + "service-type": "서비스 유형", + "no-data": "데이터 없음" + }, + "transport-messages": { + "title": "전송 메시지", + "info": "장치에서 온 모든 메시지" + }, + "activity": { + "title": "활동" + }, + "documentation": { + "title": "문서", + "add-link": "링크 추가", + "add-link-title": "문서 링크 추가", + "name": "이름", + "name-required": "이름을 입력하세요.", + "link": "링크", + "link-required": "링크가 필요합니다.", + "columns": "열" + }, + "quick-links": { + "title": "빠른 링크", + "add-link": "링크 추가", + "add-link-title": "빠른 링크 추가", + "quick-link": "빠른 링크", + "quick-link-required": "빠른 링크가 필요합니다.", + "no-links-matching": "'{{name}}'과(와) 일치하는 링크가 없습니다.", + "columns": "열" + }, + "recent-dashboards": { + "title": "대시보드", + "last": "최근 본 항목", + "starred": "즐겨찾기", + "name": "이름", + "last-viewed": "최근 본 항목", + "no-last-viewed-dashboards": "아직 최근 본 대시보드 없음" + }, + "configured-features": { + "title": "구성된 기능", + "info": "구성이 필요한 기능 상태", + "email-feature": "이메일", + "sms-feature": "SMS", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "2단계 인증", + "feature-configured": "기능이 구성되었습니다.\n클릭하여 설정", + "feature-not-configured": "기능이 구성되지 않았습니다.\n클릭하여 설정" + }, + "version-info": { + "title": "버전", + "contact-us": "문의하기", + "current-version": "현재 버전", + "current": "현재", + "available-version": "사용 가능한 버전", + "available": "사용 가능", + "upgrade": "업그레이드", + "version-is-up-to-date": "버전이 최신 상태입니다" + }, + "usage-info": { + "title": "사용량", + "entities": "엔티티", + "api-calls": "API 호출" + }, + "functions": { + "title": "함수", + "pe-feature-tooltip": "ThingsBoard\nProfessional Edition에서만", + "switch-to-pe": "PE로 전환", + "alarms": "알람", + "dashboards": "대시보드", + "entities-and-relations": "엔티티 및 관계", + "profiles": "프로필", + "advanced-features": "고급 기능", + "notification-center": "알림 센터", + "api-usage": "Api 사용량", + "customers": "고객", + "customers-hierarchy": "고객 계층 구조", + "roles-and-permissions": "역할 및 권한", + "groups": "그룹", + "integrations": "통합", + "solution-templates": "솔루션 템플릿", + "scheduler": "스케줄러", + "white-labeling": "화이트 라벨링" + }, + "devices": { + "view-docs": "문서 보기", + "inactive": "비활성", + "active": "활성", + "total": "합계" + }, + "alarms": { + "critical": "심각한", + "assigned-to-me": "나에게 할당됨", + "total": "합계" + }, + "getting-started": { + "get-started": "시작하기", + "finish": "완료", + "done-welcome-title": "환영합니다", + "done-welcome-text": "잘 해결했습니다!", + "sys-admin": { + "step1": { + "title": "테넌트 및 테넌트 관리자 생성", + "content": "

테넌트는 장치와 자산을 소유하거나 생산하는 개인 또는 조직입니다. 테넌트는 여러 테넌트 관리자 사용자, 고객, 장치 및 자산을 가질 수 있습니다.

테넌트 관리자는 테넌트 계정 내에서 장치, 자산, 고객 및 대시보드를 생성하고 관리할 수 있습니다.

방법은 문서를 참조하세요:

", + "how-to-create-tenant": "테넌트 및 테넌트 관리자 생성 방법" + }, + "step2": { + "title": "기능 구성: 메일 서버", + "content": "

메일 서버 구성은 사용자 활성화, 비밀번호 복구 및 알림 전달에 필수적입니다.

방법은 문서를 참조하세요:

", + "how-to-configure-mail-server": "메일 서버 구성 방법" + }, + "step3": { + "title": "기능 구성: SMS 공급자", + "content": "

SMS 공급자를 구성하여 고객에게 SMS로 알림을 보냅니다.

방법은 문서를 참조하세요:

", + "how-to-configure-sms-provider": "SMS 공급자 구성 방법" + }, + "step4": { + "title": "기능 구성: 2FA", + "content": "

2단계 인증으로 플랫폼 계정의 보안을 강화하세요.

방법은 문서를 참조하세요:

", + "how-to-configure-2fa": "2FA 구성 방법" + }, + "step5": { + "title": "기능 구성: OAuth 2", + "content": "

OAuth 2.0을 통한 Single Sign-On 기능으로 테넌트 및 고객 사용자의 로그인을 간소화하세요.

방법은 문서를 참조하세요:

", + "how-to-configure-oauth2": "OAuth 2 구성 방법" + }, + "step6": { + "title": "기능 구성: Slack", + "content": "

설정한 알림 규칙에 따라 Slack을 통해 테넌트 및 고객 사용자에게 알림을 배포합니다.

방법은 문서를 참조하세요:

", + "how-to-configure-notifications": "Slack 구성 방법" + } + }, + "tenant-admin": { + "step1": { + "title": "장치 생성", + "content": "

UI를 통해 플랫폼에 첫 번째 장치를 프로비저닝해 보겠습니다. 방법은 문서를 참조하세요:

", + "how-to-create-device": "장치 생성 방법" + }, + "step2": { + "title": "장치 연결", + "content-before": "

장치를 연결하려면 장치 자격 증명을 가져와야 합니다. 이 가이드에서는 기본 자동 생성 자격 증명인 액세스 토큰을 사용하는 것을 권장합니다.

  • 장치 테이블로 이동
  • 장치 행을 클릭하여 장치 세부 정보 열기
  • \"액세스 토큰 복사\" 버튼 누르기

간단한 명령을 사용하여 HTTP를 통해 데이터를 게시합니다. $ACCESS_TOKEN을 장치 액세스 토큰으로 교체하는 것을 잊지 마세요:

", + "ubuntu": { + "install-curl": "Ubuntu용 cURL 설치:" + }, + "macos": { + "install-curl": "MacOS용 cURL 설치:" + }, + "windows": { + "install-curl": "Windows 10 b17063부터 cURL이 기본적으로 사용 가능합니다." + }, + "replace-access-token": "$ACCESS_TOKEN을 장치의 토큰으로 교체:", + "content-after": "

MQTT, CoAP 등 다른 프로토콜도 사용할 수 있습니다.

방법은 문서를 참조하세요:

", + "how-to-connect-device": "장치 연결 방법" + }, + "step3": { + "title": "대시보드 생성", + "content": "

자산, 장치 등의 엔티티에서 데이터를 시각화하는 대시보드를 만듭니다.

방법은 문서를 참조하세요:

", + "how-to-create-dashboard": "대시보드 생성 방법" + }, + "step4": { + "title": "알람 규칙 구성", + "alarm-rules": "알람 규칙", + "content": "

온도가 25°C에 도달하면 알림을 발생시켜 보겠습니다. 방법은 문서를 참조하세요:

", + "how-to-configure-alarm-rules": "알림 규칙 구성 방법" + }, + "step5": { + "title": "알람 생성", + "content-before": "

알림을 트리거하려면 26°C 이상의 새 텔레메트리 값을 보냅니다.

", + "replace-access-token": "$ACCESS_TOKEN을 장치의 토큰으로 교체:", + "content-after": "

방법은 문서를 참조하세요:

", + "how-to-create-alarm": "알림 생성 방법" + }, + "step6": { + "title": "고객 생성 및 대시보드 할당", + "content": "

최종 사용자 대시보드를 생성하면 고객 사용자는 자신의 장치만 볼 수 있으며 다른 고객의 데이터는 숨겨집니다.

방법은 문서를 참조하세요:

", + "how-to-create-customer-and-assign-dashboard": "고객 생성 및 대시보드 할당 방법" + } + } + }, + "api-usage": { + "api-usage": "Api 사용량", + "label": "라벨", + "state-name": "상태 이름", + "status": "상태", + "status-required": "상태는 필수 항목입니다.", + "limit": "최대 제한", + "limit-required": "최대 제한이 필요합니다.", + "current-number": "현재 수", + "current-number-required": "현재 수는 필수 항목입니다.", + "add-key": "키 추가", + "no-key": "키 없음", + "delete-key": "키 삭제", + "target-dashboard-state": "대상 대시보드 상태", + "go-to-main-state": "기본 보기로 이동" + }, + "unit-conversion": { + "hint": "저장된 값의 단위입니다. 변환할 원본 단위입니다. 소스 데이터에서 사용하는 기호를 입력하세요 (예: m, km, ft, in)." } }, + "color": { + "color": "색상" + }, "icon": { "icon": "아이콘", + "icons": "아이콘", + "custom": "사용자 지정", "select-icon": "선택된 아이콘", - "material-icons": "Material icons", - "show-all": "모든 아이콘 보기" + "material-icons": "머티리얼 아이콘", + "show-all": "모든 아이콘 보기", + "search-icon": "아이콘 검색", + "no-icons-found": "'{{iconSearch}}'에 대한 아이콘을 찾을 수 없음" + }, + "phone-input": { + "phone-input-label": "전화번호", + "phone-input-required": "전화번호는 필수 항목입니다.", + "phone-input-validation": "전화번호가 유효하지 않거나 불가능합니다.", + "phone-input-pattern": "잘못된 전화번호입니다. E.164 형식을 사용하세요 (예: {{phoneNumber}})", + "phone-input-hint": "E.164 형식의 전화번호 (예: {{phoneNumber}})" }, "custom": { "widget-action": { - "action-cell-button": "Action cell button", - "row-click": "On row click", - "polygon-click": "On polygon click", - "marker-click": "On marker click", - "tooltip-tag-action": "Tooltip tag action", - "node-selected": "On node selected", - "element-click": "On HTML element click", - "pie-slice-click": "On slice click", - "row-double-click": "On row double click" + "action-cell-button": "동작 셀 버튼", + "row-click": "행 클릭 시", + "cell-click": "셀 클릭 시", + "polygon-click": "다각형 클릭 시", + "marker-click": "마커 클릭 시", + "circle-click": "원 클릭 시", + "tooltip-tag-action": "툴팁 태그 동작", + "node-selected": "노드 선택 시", + "element-click": "HTML 요소 클릭 시", + "pie-slice-click": "슬라이스 클릭 시", + "row-double-click": "행 더블 클릭 시", + "cell-double-click": "셀 더블 클릭 시", + "card-click": "카드 클릭 시", + "click": "클릭 시" } }, + "paginator": { + "items-per-page": "페이지당 항목:", + "first-page-label": "첫 페이지", + "last-page-label": "마지막 페이지", + "next-page-label": "다음 페이지", + "previous-page-label": "이전 페이지", + "items-per-page-separator": "/" + }, "language": { - "language": "언어(Language)" + "auto": "자동", + "language": "언어(Language)", + "locales": { + "ar_AE": "العربية (الإمارات العربية المتحدة)", + "ca_ES": "català (Espanya)", + "cs_CZ": "čeština (Česko)", + "da_DK": "dansk (Danmark)", + "de_DE": "Deutsch (Deutschland)", + "el_GR": "Ελληνικά (Ελλάδα)", + "en_US": "English (United States)", + "es_ES": "español (España)", + "fa_IR": "فارسی (ایران)", + "fr_FR": "français (France)", + "hi_IN": "हिन्दी (भारत)", + "it_IT": "italiano (Italia)", + "ja_JP": "日本語 (日本)", + "ka_GE": "ქართული (საქართველო)", + "ko_KR": "한국어 (대한민국)", + "lt_LT": "lietuvių (Lietuva)", + "lv_LV": "latviešu (Latvija)", + "nl_BE": "Nederlands (België)", + "nl_NL": "Nederlands (Nederland)", + "no_NO": "norsk (Norge)", + "pl_PL": "polski (Polska)", + "pt_BR": "português (Brasil)", + "ro_RO": "română (România)", + "sl_SI": "slovenščina (Slovenija)", + "tr_TR": "Türkçe (Türkiye)", + "uk_UA": "українська (Україна)", + "zh_CN": "中文 (中国)", + "zh_TW": "中文 (台灣)" + } } } From f2a0d53118da47ef9e899653568fd7aae80cb03f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 16 Mar 2026 11:18:40 +0200 Subject: [PATCH 159/313] fixed schema ordering for generic classes --- .../server/config/SwaggerConfiguration.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index dee30e5e09..d1c036d38e 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -112,7 +112,7 @@ public class SwaggerConfiguration { private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true); // Populated by mapAwareConverter, consumed by customOpenApiCustomizer. - // Keyed by Class.getSimpleName() which matches swagger-core's default schema naming. + // Keyed by the schema name that swagger-core generates (see resolveSchemaName). private final Map> schemaPropertyOrders = new ConcurrentHashMap<>(); private final Map> schemaOwnProps = new ConcurrentHashMap<>(); @@ -325,10 +325,11 @@ public class SwaggerConfiguration { // (including ones where the ModelConverter only sees a $ref). try { var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType); - schemaPropertyOrders.put(cls.getSimpleName(), resolvePropertyOrder(cls, beanDesc)); + String schemaName = resolveSchemaName(javaType); + schemaPropertyOrders.put(schemaName, resolvePropertyOrder(cls, beanDesc)); Set ownProps = computeOwnPropNames(cls, beanDesc); if (!ownProps.isEmpty()) { - schemaOwnProps.put(cls.getSimpleName(), ownProps); + schemaOwnProps.put(schemaName, ownProps); } } catch (Exception e) { log.debug("Failed to resolve property order for {}: {}", cls.getName(), e.getMessage()); @@ -790,6 +791,26 @@ public class SwaggerConfiguration { } } + /** + * Computes the schema name that swagger-core will use for the given JavaType. + * For simple types, this is just the class simple name (e.g. {@code Device}). + * For parameterized types, type parameter names are appended + * (e.g. {@code PageData} becomes {@code PageDataDevice}). + * This matches the naming convention used by swagger-core's {@code TypeNameResolver}. + */ + private static String resolveSchemaName(JavaType javaType) { + StringBuilder sb = new StringBuilder(javaType.getRawClass().getSimpleName()); + if (javaType.hasGenericTypes()) { + for (int i = 0; i < javaType.containedTypeCount(); i++) { + JavaType param = javaType.containedType(i); + if (param != null) { + sb.append(param.getRawClass().getSimpleName()); + } + } + } + return sb.toString(); + } + /** * Returns the JSON property names that are backed by fields declared directly in {@code cls} * (not inherited from a superclass). Used to distinguish "own" from "inherited" properties From 06bf79234af15c85dbcdfc5a0ada78a9a528acd5 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 16 Mar 2026 11:51:36 +0200 Subject: [PATCH 160/313] Code review changes --- .../service/edge/EdgeContextComponent.java | 6 +-- .../edge/rpc/AttributeSaveCallback.java | 43 ------------------- .../edge/rpc/DownlinkMessageMapper.java | 8 +++- .../edge/rpc/EdgeUplinkMessageDispatcher.java | 4 ++ .../server/service/edge/rpc/GrpcServer.java | 4 ++ .../edge/rpc/service/EdgeGrpcService.java | 4 ++ .../DefaultZombieSessionCleanupService.java | 4 ++ .../edge/rpc/session/EdgeGrpcSession.java | 3 +- .../edge/rpc/session/EdgeSessionsHolder.java | 5 +++ .../KafkaBasedEdgeGrpcSessionManager.java | 3 +- .../PostgresBasedEdgeGrpcSessionManager.java | 2 +- 11 files changed, 34 insertions(+), 52 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/AttributeSaveCallback.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 d3da4f7f15..065c3f2cec 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 @@ -99,15 +99,13 @@ import java.util.concurrent.ScheduledExecutorService; @TbCoreComponent public class EdgeContextComponent { - @Value("${edges.scheduler_pool_size}") - private int schedulerPoolSize; - private final Map processorMap = new EnumMap<>(EdgeEventType.class); private ScheduledExecutorService edgeEventProcessingExecutorService; @Autowired - public EdgeContextComponent(List processors) { + public EdgeContextComponent(List processors, + @Value("${edges.scheduler_pool_size}") int schedulerPoolSize) { processors.forEach(processor -> { EdgeEventType eventType = processor.getEdgeEventType(); if (eventType != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/AttributeSaveCallback.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/AttributeSaveCallback.java deleted file mode 100644 index 7d0f14c24e..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/AttributeSaveCallback.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc; - -import com.google.common.util.concurrent.FutureCallback; -import jakarta.annotation.Nullable; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.TenantId; - -@Slf4j -@AllArgsConstructor -public class AttributeSaveCallback implements FutureCallback { - - private final TenantId tenantId; - private final EdgeId edgeId; - private final String key; - private final Object value; - - @Override - public void onSuccess(@Nullable Void result) { - log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value); - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java index 5c6d675f69..a83e3036f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/DownlinkMessageMapper.java @@ -17,12 +17,15 @@ package org.thingsboard.server.service.edge.rpc; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.ArrayList; @@ -31,13 +34,16 @@ import java.util.List; @Component @Slf4j @RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent public class DownlinkMessageMapper { private final EdgeContextComponent ctx; public List convertToDownlinkMsgsPack(EdgeSessionState state, List edgeEvents) { List result = new ArrayList<>(); - for (EdgeEvent edgeEvent : edgeEvents) { + List filtered = EdgeMsgConstructorUtils.mergeAndFilterDownlinkDuplicates(edgeEvents); + for (EdgeEvent edgeEvent : filtered) { log.trace("[{}][{}] converting edge event to downlink msg [{}]", state.getTenantId(), state.getEdgeId(), edgeEvent); DownlinkMsg downlinkMsg = null; try { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java index 852a6f2ad7..e5dd0738ac 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeUplinkMessageDispatcher.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; @@ -51,6 +52,7 @@ import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeContextComponent; import java.util.ArrayList; @@ -59,6 +61,8 @@ import java.util.List; @Service @Slf4j @RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent public class EdgeUplinkMessageDispatcher { private final EdgeContextComponent ctx; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java index 4ca8a4f838..c5c2e40f5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java @@ -24,11 +24,13 @@ import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.transport.config.ssl.PemSslCredentials; import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc.EdgeRpcServiceImplBase; import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.TbCoreComponent; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -36,6 +38,8 @@ import java.util.concurrent.TimeUnit; @Component @Slf4j @RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent public class GrpcServer { @Value("${edges.rpc.port}") diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java index f9ecac9923..a41ff78110 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java @@ -23,6 +23,7 @@ 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.condition.ConditionalOnProperty; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -54,6 +55,7 @@ import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; import org.thingsboard.server.gen.edge.v1.RequestMsg; import org.thingsboard.server.gen.edge.v1.ResponseMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.EdgeSessionState; @@ -77,6 +79,8 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS @Service @Slf4j @RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { @Value("${edges.send_scheduler_pool_size}") diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java index 6aac5646d8..76d554eab9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java @@ -18,8 +18,10 @@ package org.thingsboard.server.service.edge.rpc.session; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeSessionState; import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; import org.thingsboard.server.service.edge.rpc.session.manager.KafkaBasedEdgeGrpcSessionManager; @@ -35,6 +37,8 @@ import java.util.function.Function; @Service @Slf4j +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent public class DefaultZombieSessionCleanupService implements ZombieSessionCleanupService { @Autowired diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java index f6705b13e2..dc2296f1d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java @@ -58,7 +58,6 @@ import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; -import org.thingsboard.server.service.edge.rpc.AttributeSaveCallback; import org.thingsboard.server.service.edge.rpc.DownlinkMessageMapper; import org.thingsboard.server.service.edge.rpc.EdgeSessionState; import org.thingsboard.server.service.edge.rpc.EdgeSyncCursor; @@ -604,7 +603,7 @@ public class EdgeGrpcSession implements EdgeSession { .entityId(getEdgeId()) .scope(AttributeScope.SERVER_SCOPE) .entry(new BooleanDataEntry(DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY, value)) - .callback(new AttributeSaveCallback(getTenantId(), getEdgeId(), DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY, value)) + .callback(new EdgeAttributeSaveCallback(getTenantId(), getEdgeId(), DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY, value)) .build()); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java index 332da77ad6..7cc27214a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java @@ -17,8 +17,10 @@ package org.thingsboard.server.service.edge.rpc.session; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeSessionState; import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; @@ -30,6 +32,8 @@ import java.util.function.Consumer; @Data @Slf4j @Component +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent public class EdgeSessionsHolder { private final ConcurrentMap sessions = new ConcurrentHashMap<>(); @@ -66,6 +70,7 @@ public class EdgeSessionsHolder { public void remove(EdgeGrpcSessionManager session) { if (session == null) { log.warn("Can't remove session from holder because it's null"); + return; } EdgeSessionState sessionState = session.getState(); removeByEdgeId(sessionState.getEdge().getId()); // todo: react to warnings diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java index 91c33cfc6d..186510d5bb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java @@ -167,9 +167,10 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan try { isHighPriorityProcessing = true; session.processHighPriorityEvents(); - isHighPriorityProcessing = false; } catch (Exception e) { log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); + } finally { + isHighPriorityProcessing = false; } }, NO_INITIAL_DELAY_VALUE, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); highPriorityProcessingFutureRef.set(highPriorityProcessingTask); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java index 5b86e309e6..242f513e10 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java @@ -168,7 +168,7 @@ public class PostgresBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSession private void markHasEvents(boolean newEventsPresent) { newEventsLock.lock(); try { - if (!hasNewEvents) { + if (hasNewEvents != newEventsPresent) { log.trace("[{}] set session new events flag to {} [{}]", getState().getTenantId(), newEventsPresent, getState().getEdgeId()); hasNewEvents = newEventsPresent; } From c09c8214119b6d3a4e68a2d626df89aa6c97f2a2 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Tue, 17 Mar 2026 10:54:50 +0200 Subject: [PATCH 161/313] Fix EdgeGrpcSslTest --- .../thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java index 6c72693dcb..7614bdd310 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java @@ -171,7 +171,8 @@ class EdgeGrpcSslTest { Path certOnlyFile = writeTempPem("cert-only", cert); assertThatThrownBy(() -> startServer(certOnlyFile.toString(), "", null)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(RuntimeException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); } // --- Server startup using production EdgeGrpcService.setupSsl() --- From dc7b86fa1da0b0be125be22f0698bce41ea35d4b Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Wed, 18 Mar 2026 11:00:35 +0200 Subject: [PATCH 162/313] Address PR review comments for edge session refactoring - Deduplicate forEach iteration over both session maps (HashSet) - Use null-safe getEdgeId() instead of getEdge().getId() in remove() - Add defensive null checks on getState() in onConfigurationUpdate and destroyAndMarkAsZombieIfFailed - Add null check on pendingMsgsMap.get() in onDownlinkResponse - Move scheduleAtFixedRate to @PostConstruct in DefaultZombieSessionCleanupService - Fix misleading "kafka sessions" log message to "zombie sessions" - Chain IOException in GrpcServer RuntimeException - Guard against race condition on removeByEdgeId in onEdgeDisconnect - Rename shadowed parameter in shutdownExecutorSafely --- .../server/service/edge/rpc/GrpcServer.java | 2 +- .../edge/rpc/service/EdgeGrpcService.java | 12 +++++---- .../DefaultZombieSessionCleanupService.java | 7 +++++- .../edge/rpc/session/EdgeGrpcSession.java | 2 +- .../edge/rpc/session/EdgeSessionsHolder.java | 10 +++++--- .../AbstractEdgeGrpcSessionManager.java | 25 +++++++++++++------ 6 files changed, 39 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java index c5c2e40f5b..0b19f10aa4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/GrpcServer.java @@ -85,7 +85,7 @@ public class GrpcServer { server.start(); } catch (IOException e) { log.error("Failed to start Edge RPC server!", e); - throw new RuntimeException("Failed to start Edge RPC server!"); + throw new RuntimeException("Failed to start Edge RPC server!", e); } log.info("Edge RPC server initialized!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java index a41ff78110..b50d643634 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/service/EdgeGrpcService.java @@ -221,8 +221,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i EdgeGrpcSessionManager current = sessions.getByEdgeId(edgeId); if (current != null && current.getState().getSessionId().equals(sessionId)) { EdgeGrpcSessionManager toRemove = sessions.removeByEdgeId(edgeId); - toRemove.onEdgeDisconnect(); - toRemove.destroyAndMarkAsZombieIfFailed(); + if (toRemove != null) { + toRemove.onEdgeDisconnect(); + toRemove.destroyAndMarkAsZombieIfFailed(); + } sessions.removeBySessionId(sessionId); save(tenantId, edgeId, ACTIVITY_STATE, false); long lastDisconnectTs = System.currentTimeMillis(); @@ -382,9 +384,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } - private void shutdownExecutorSafely(ExecutorService sendDownlinkExecutorService) { - if (sendDownlinkExecutorService != null && !sendDownlinkExecutorService.isShutdown()) { - sendDownlinkExecutorService.shutdown(); + private void shutdownExecutorSafely(ExecutorService e) { + if (e != null && !e.isShutdown()) { + e.shutdown(); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java index 76d554eab9..780dcbe196 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/DefaultZombieSessionCleanupService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.edge.rpc.session; +import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -50,6 +51,10 @@ public class DefaultZombieSessionCleanupService implements ZombieSessionCleanupS public DefaultZombieSessionCleanupService() { this.zombieSessions = new ConcurrentLinkedQueue<>(); this.zombieSessionsExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("zombie-sessions"); + } + + @PostConstruct + public void init() { this.zombieSessionsExecutorService.scheduleAtFixedRate(this::cleanupZombieSessions, 30, 60, TimeUnit.SECONDS); } @@ -86,7 +91,7 @@ public class DefaultZombieSessionCleanupService implements ZombieSessionCleanupS } }); } catch (Exception e) { - log.warn("Failed to cleanup kafka sessions", e); + log.warn("Failed to cleanup zombie sessions", e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java index dc2296f1d5..73a3b53e4c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeGrpcSession.java @@ -334,7 +334,7 @@ public class EdgeGrpcSession implements EdgeSession { log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", getTenantId(), getEdgeId(), getSessionId(), msg.getDownlinkMsgId(), msg.getErrorMsg()); DownlinkMsg downlinkMsg = state.getPendingMsgsMap().get(msg.getDownlinkMsgId()); // if NOT timeseries or attributes failures - ack failed downlink - if (downlinkMsg.getEntityDataCount() == 0) { + if (downlinkMsg != null && downlinkMsg.getEntityDataCount() == 0) { state.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java index 7cc27214a7..2d7c3dd8e3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/EdgeSessionsHolder.java @@ -24,6 +24,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeSessionState; import org.thingsboard.server.service.edge.rpc.session.manager.EdgeGrpcSessionManager; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -40,8 +42,10 @@ public class EdgeSessionsHolder { private final ConcurrentMap sessionsById = new ConcurrentHashMap<>(); public void forEach(Consumer consumer) { - sessions.values().forEach(consumer); - sessionsById.values().forEach(consumer); + Set unique = new HashSet<>(sessions.values()); + unique.addAll(sessionsById.values()); + + unique.forEach(consumer); } public void put(EdgeGrpcSessionManager session) { @@ -73,7 +77,7 @@ public class EdgeSessionsHolder { return; } EdgeSessionState sessionState = session.getState(); - removeByEdgeId(sessionState.getEdge().getId()); // todo: react to warnings + removeByEdgeId(sessionState.getEdgeId()); removeBySessionId(sessionState.getSessionId()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java index 1634d2fab4..0df1af1ada 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/AbstractEdgeGrpcSessionManager.java @@ -101,11 +101,12 @@ public abstract class AbstractEdgeGrpcSessionManager extends EdgeGrpcSessionDele @Override public void onConfigurationUpdate(Edge edge) { EdgeSessionState state = getState(); - CustomerId stateCustomerId = null; - if (state != null) { - stateCustomerId = state.getEdge().getCustomerId(); - state.setEdge(edge); + if (state == null) { + log.warn("Session state is null, skipping configuration update for edge [{}]", edge.getId()); + return; } + CustomerId stateCustomerId = state.getEdge().getCustomerId(); + state.setEdge(edge); if (stateCustomerId != null && !stateCustomerId.equals(edge.getCustomerId())) { // do not send edge configuration message on customer update // message send by separate flow from assign_to or unassing_from customer @@ -128,13 +129,21 @@ public abstract class AbstractEdgeGrpcSessionManager extends EdgeGrpcSessionDele try (finalSession) { if (!destroy()) { - log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", - state.getTenantId(), state.getEdgeId(), state.getEdge().getName(), state.getSessionId()); + if (state != null) { + log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", + state.getTenantId(), state.getEdgeId(), state.getEdge().getName(), state.getSessionId()); + } else { + log.warn("Session destroy failed (state is null). Adding to zombie queue for later cleanup."); + } zombieSessionCleanupService.add(this); } } catch (Exception e) { - log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]", - state.getTenantId(), state.getEdgeId(), state.getEdge().getName(), state.getSessionId(), e); + if (state != null) { + log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]", + state.getTenantId(), state.getEdgeId(), state.getEdge().getName(), state.getSessionId(), e); + } else { + log.warn("Exception during session destroy (state is null)", e); + } } } } From d915dbeb03a8b6c57b955d372ca9e2f41f550be4 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Wed, 18 Mar 2026 11:11:47 +0200 Subject: [PATCH 163/313] Fix potential NPE on getState() in PostgresBasedEdgeGrpcSessionManager --- .../manager/PostgresBasedEdgeGrpcSessionManager.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java index 242f513e10..13935f41cf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java @@ -27,6 +27,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; import org.thingsboard.server.service.edge.rpc.processor.PostgresGeneralEdgeEventsDispatcher; import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; @@ -148,8 +149,8 @@ public class PostgresBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSession } private void cancelScheduleEdgeEventsCheck() { - EdgeId edgeId = getState().getEdgeId(); - log.trace("[{}] cancelling edge event check for edge", edgeId); + EdgeSessionState state = getState(); + log.trace("[{}] cancelling edge event check for edge", state != null ? state.getEdgeId() : "unknown"); ScheduledFuture sf = edgeEventCheckFutureRef.getAndSet(null); if (sf != null && !sf.isCancelled() && !sf.isDone()) { @@ -169,7 +170,10 @@ public class PostgresBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSession newEventsLock.lock(); try { if (hasNewEvents != newEventsPresent) { - log.trace("[{}] set session new events flag to {} [{}]", getState().getTenantId(), newEventsPresent, getState().getEdgeId()); + EdgeSessionState state = getState(); + if (state != null) { + log.trace("[{}] set session new events flag to {} [{}]", state.getTenantId(), newEventsPresent, state.getEdgeId()); + } hasNewEvents = newEventsPresent; } } finally { From 2647b807f64cc702f0884da908f755ca4a48f892 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Wed, 18 Mar 2026 11:16:51 +0200 Subject: [PATCH 164/313] Remove shadowing downlinkMessageMapper field in KafkaBasedEdgeGrpcSessionManager --- .../manager/KafkaBasedEdgeGrpcSessionManager.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java index 186510d5bb..d688f1df4a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java @@ -35,7 +35,7 @@ import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbKafkaComponent; -import org.thingsboard.server.service.edge.rpc.DownlinkMessageMapper; + import org.thingsboard.server.service.edge.rpc.EdgeSessionState; import org.thingsboard.server.service.edge.rpc.processor.PostgresGeneralEdgeEventsDispatcher; import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; @@ -61,7 +61,6 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan private static final int NO_INITIAL_DELAY_VALUE = 0; private final TbCoreQueueFactory tbCoreQueueFactory; - private final DownlinkMessageMapper downlinkMessageMapper; private final TopicService topicService; private final KafkaAdmin kafkaAdmin; private final EdgeSessionsHolder sessions; @@ -252,12 +251,14 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan } private void cancelMigrationAndProcessingInit() { - log.trace("[{}] cancelling edge migration & processing init for edge", getState().getEdgeId()); + EdgeSessionState state = getState(); + log.trace("[{}] cancelling edge migration & processing init for edge", state != null ? state.getEdgeId() : "unknown"); cancelFuture(initMigrationAndProcessingFutureRef); } private void cancelHighPriorityProcessing() { - log.trace("[{}] cancelling high priority processing task for edge", getState().getEdgeId()); + EdgeSessionState state = getState(); + log.trace("[{}] cancelling high priority processing task for edge", state != null ? state.getEdgeId() : "unknown"); cancelFuture(highPriorityProcessingFutureRef); } From 4e32c6490a2028e2e148c3018e1d9e9899fcbaf0 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Wed, 18 Mar 2026 11:30:44 +0200 Subject: [PATCH 165/313] Restore interrupt flag in awaitConsumerTermination --- .../rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java index d688f1df4a..ebc5531320 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java @@ -292,10 +292,11 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan EdgeSessionState state = getState(); try { if (!consumerExecutor.awaitTermination(10, java.util.concurrent.TimeUnit.SECONDS)) { - consumerExecutor.shutdownNow(); // todo: verify it is acceptable + consumerExecutor.shutdownNow(); } } catch (InterruptedException ie) { log.warn("[{}][{}] Interrupted while awaiting consumer executor termination", state.getTenantId(), state.getEdgeId()); + Thread.currentThread().interrupt(); } } } From 511d709b03f8ce8b8d35ef0968c06826ac65fdb3 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Wed, 18 Mar 2026 11:54:58 +0200 Subject: [PATCH 166/313] Remove stale todo in PostgresBasedEdgeGrpcSessionManager --- .../rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java index 13935f41cf..0190cd2784 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/PostgresBasedEdgeGrpcSessionManager.java @@ -46,7 +46,6 @@ public class PostgresBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSession private final EdgeSessionsHolder edgeSessions; - // todo: verify handled properly private final Lock newEventsLock = new ReentrantLock(); private final AtomicReference> edgeEventCheckFutureRef = new AtomicReference<>(); private PostgresGeneralEdgeEventsDispatcher generalEdgeEventsDispatcher; From 63f9c05f04fde74db413ffefc04981f358a2882c Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 18 Mar 2026 12:46:22 +0200 Subject: [PATCH 167/313] Tests for new ThingsboardClient --- application/pom.xml | 5 + .../server/client/AIModelJavaClientTest.java | 168 ++++++++++ .../server/client/AbstractJavaClientTest.java | 127 ++++++++ .../server/client/AdminJavaClientTest.java | 100 ++++++ .../client/AlarmCommentJavaClientTest.java | 106 +++++++ .../server/client/AlarmJavaClientTest.java | 163 ++++++++++ .../server/client/AssetJavaClientTest.java | 84 +++++ .../client/AssetProfileJavaClientTest.java | 135 ++++++++ .../client/CalculatedFieldJavaClientTest.java | 286 +++++++++++++++++ .../server/client/CustomerJavaClientTest.java | 113 +++++++ .../client/DashboardJavaClientTest.java | 123 ++++++++ .../DeviceConnectivityJavaClientTest.java | 53 ++++ .../server/client/DeviceJavaClientTest.java | 114 +++++++ .../client/DeviceProfileJavaClientTest.java | 157 ++++++++++ .../server/client/DomainJavaClientTest.java | 105 +++++++ .../server/client/EdgeJavaClientTest.java | 141 +++++++++ .../client/EntityQueryJavaClientTest.java | 289 ++++++++++++++++++ .../client/EntityRelationJavaClientTest.java | 182 +++++++++++ .../client/EntityViewJavaClientTest.java | 267 ++++++++++++++++ .../client/MobileAppJavaClientTest.java | 156 ++++++++++ .../client/NotificationJavaClientTest.java | 278 +++++++++++++++++ .../server/client/Oauth2JavaClientTest.java | 138 +++++++++ .../client/OtaPackageJavaClientTest.java | 261 ++++++++++++++++ .../server/client/RpcV1JavaClientTest.java | 72 +++++ .../server/client/RpcV2JavaClientTest.java | 133 ++++++++ .../client/RuleChainJavaClientTest.java | 163 ++++++++++ .../server/client/TbImageJavaClientTest.java | 151 +++++++++ .../client/TbResourceJavaClientTest.java | 128 ++++++++ .../client/TelemetryJavaClientTest.java | 150 +++++++++ .../server/client/TenantJavaClientTest.java | 119 ++++++++ .../client/TenantProfileJavaClientTest.java | 179 +++++++++++ .../client/TwoFactorAuthJavaClientTest.java | 78 +++++ .../server/client/UserJavaClientTest.java | 135 ++++++++ .../client/WidgetTypeJavaClientTest.java | 155 ++++++++++ pom.xml | 7 + 35 files changed, 5021 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java create mode 100644 application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java diff --git a/application/pom.xml b/application/pom.xml index 51c267306d..1926dd5a92 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -290,6 +290,11 @@ rest-client test + + org.thingsboard.client + thingsboard-ce-client + test + org.springframework.security spring-security-test diff --git a/application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java new file mode 100644 index 0000000000..5a0963aa3f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java @@ -0,0 +1,168 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AiModel; +import org.thingsboard.client.model.OpenAiChatModelConfig; +import org.thingsboard.client.model.OpenAiProviderConfig; +import org.thingsboard.client.model.PageDataAiModel; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AIModelJavaClientTest extends AbstractJavaClientTest { + + private static final String AI_PREFIX = "AiTest_"; + + @Test + public void testSaveAndGetAiModel() throws Exception { + long ts = System.currentTimeMillis(); + String name = AI_PREFIX + "save_" + ts; + + AiModel model = buildAiModel(name, "gpt-4o", 0.7); + AiModel saved = client.saveAiModel(model); + assertNotNull(saved); + assertNotNull(saved.getId()); + assertEquals(name, saved.getName()); + assertNotNull(saved.getConfiguration()); + + // get by id + AiModel fetched = client.getAiModelById(saved.getId().getId()); + assertNotNull(fetched); + assertEquals(name, fetched.getName()); + assertEquals(saved.getId().getId(), fetched.getId().getId()); + } + + @Test + public void testGetAiModelById() throws Exception { + long ts = System.currentTimeMillis(); + AiModel saved = createAiModel("getbyid_" + ts); + + AiModel fetched = client.getAiModelById(saved.getId().getId()); + assertNotNull(fetched); + assertEquals(saved.getName(), fetched.getName()); + assertEquals(saved.getId().getId(), fetched.getId().getId()); + } + + @Test + public void testUpdateAiModel() throws Exception { + long ts = System.currentTimeMillis(); + AiModel saved = createAiModel("update_" + ts); + + saved.setName(AI_PREFIX + "updated_" + ts); + OpenAiChatModelConfig updatedConfig = new OpenAiChatModelConfig(); + updatedConfig.setModelId("gpt-4o-mini"); + updatedConfig.setTemperature(0.3); + updatedConfig.setMaxOutputTokens(2048); + updatedConfig.setMaxRetries(50); + OpenAiProviderConfig providerConfig = new OpenAiProviderConfig(); + providerConfig.setApiKey("test-api-key"); + providerConfig.setBaseUrl("https://api.openai.com/v1"); + updatedConfig.setProviderConfig(providerConfig); + updatedConfig.setProvider("OPENAI"); + saved.setConfiguration(updatedConfig); + + AiModel updated = client.saveAiModel(saved); + assertNotNull(updated); + assertEquals(saved.getId().getId(), updated.getId().getId()); + assertEquals(AI_PREFIX + "updated_" + ts, updated.getName()); + } + + @Test + public void testDeleteAiModel() throws Exception { + long ts = System.currentTimeMillis(); + AiModel saved = createAiModel("delete_" + ts); + + UUID modelId = saved.getId().getId(); + client.getAiModelById(modelId); + + Boolean deleted = client.deleteAiModelById(modelId); + assertTrue(deleted); + + assertReturns404(() -> client.getAiModelById(modelId)); + } + + @Test + public void testGetAiModels() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 3; i++) { + createAiModel("list_" + ts + "_" + i); + } + + PageDataAiModel page = client.getAiModels(100, 0, AI_PREFIX + "list_" + ts, null, null); + assertNotNull(page); + assertEquals(3, page.getTotalElements().intValue()); + for (AiModel m : page.getData()) { + assertTrue(m.getName().startsWith(AI_PREFIX + "list_" + ts)); + } + } + + @Test + public void testGetAiModelById_notFound() { + UUID nonExistentId = UUID.randomUUID(); + assertReturns404(() -> client.getAiModelById(nonExistentId)); + } + + @Test + public void testGetAiModelsPagination() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 5; i++) { + createAiModel("paged_" + ts + "_" + i); + } + + PageDataAiModel page1 = client.getAiModels(2, 0, AI_PREFIX + "paged_" + ts, null, null); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + PageDataAiModel lastPage = client.getAiModels(2, 2, AI_PREFIX + "paged_" + ts, null, null); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + + private AiModel buildAiModel(String name, String modelId, double temperature) { + OpenAiChatModelConfig config = new OpenAiChatModelConfig(); + config.setModelId(modelId); + config.setTemperature(temperature); + config.setMaxRetries(50); + OpenAiProviderConfig openAiProviderConfig = new OpenAiProviderConfig(); + openAiProviderConfig.setApiKey("test-api-key"); + openAiProviderConfig.setBaseUrl("https://api.openai.com/v1"); + config.setProviderConfig(openAiProviderConfig); + config.setProvider("OPENAI"); + + AiModel model = new AiModel(); + model.setName(name); + model.setConfiguration(config); + return model; + } + + private AiModel createAiModel(String suffix) throws Exception { + return client.saveAiModel(buildAiModel(AI_PREFIX + suffix, "gpt-4o", 0.7)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java new file mode 100644 index 0000000000..8323915184 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.ThingsboardClient; +import org.thingsboard.client.model.ActivateUserRequest; +import org.thingsboard.client.model.Authority; +import org.thingsboard.client.model.JwtPair; +import org.thingsboard.client.model.User; +import org.thingsboard.client.model.UserId; +import org.thingsboard.server.common.data.util.ThrowingRunnable; +import org.thingsboard.server.controller.AbstractControllerTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@Slf4j +public abstract class AbstractJavaClientTest extends AbstractControllerTest { + + protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + protected static final ObjectMapper MAPPER = new ObjectMapper(); + + protected static final String TEST_PREFIX = "JavaClientTestDevice_"; + protected static final String TEST_PREFIX_2 = "JavaClientTestDevice2_"; + protected static final String CUSTOMER_USERNAME = "javaClientCustomer@thingsboard.org"; + protected static final String TENANT_ADMIN_USERNAME = "javaClientTenant@thingsboard.org"; + protected static final String TEST_PASSWORD = "password123"; + + protected ThingsboardClient client; + + // FQN for Tenant/Customer to avoid collision with AbstractWebTest fields + protected org.thingsboard.client.model.Tenant savedClientTenant; + protected User clientTenantAdmin; + protected org.thingsboard.client.model.Customer savedClientCustomer; + protected User savedClientCustomerUser; + + @Before + public void setUpJavaClient() throws Exception { + client = ThingsboardClient.builder() + .url("http://localhost:" + wsPort) + .build(); + client.login("sysadmin@thingsboard.org", "sysadmin"); + + org.thingsboard.client.model.Tenant tenant = new org.thingsboard.client.model.Tenant(); + tenant.setTitle("Java client test tenant"); + savedClientTenant = client.saveTenant(tenant); + + clientTenantAdmin = new User(); + clientTenantAdmin.setAuthority(Authority.TENANT_ADMIN); + clientTenantAdmin.setTenantId(savedClientTenant.getId()); + clientTenantAdmin.setEmail(TENANT_ADMIN_USERNAME); + clientTenantAdmin = client.saveUser(clientTenantAdmin, "false"); + activateUserAndAuthorize(clientTenantAdmin); + + org.thingsboard.client.model.Customer customer = new org.thingsboard.client.model.Customer(); + customer.setTitle("Java client test customer"); + customer.setTenantId(savedClientTenant.getId()); + savedClientCustomer = client.saveCustomer(customer, null, null, null); + + User customerUser = new User(); + customerUser.setAuthority(Authority.CUSTOMER_USER); + customerUser.setTenantId(savedClientTenant.getId()); + customerUser.setCustomerId(savedClientCustomer.getId()); + customerUser.setEmail(CUSTOMER_USERNAME); + savedClientCustomerUser = client.saveUser(customerUser, "false"); + activateUser(savedClientCustomerUser.getId(), "password123", false); + } + + @After + public void tearDownJavaClient() { + client.login("sysadmin@thingsboard.org", "sysadmin"); + client.deleteTenant(savedClientTenant.getId().getId().toString()); + } + + protected String getBaseUrl() { + return "http://localhost:" + wsPort; + } + + protected void activateUserAndAuthorize(User user) throws ApiException { + JwtPair jwtPair = activateUser(user.getId(), TEST_PASSWORD, false); + client.setToken(jwtPair.getToken()); + } + + protected JwtPair activateUser(UserId userId, String password, boolean sendActivationMail) throws ApiException { + ActivateUserRequest activateRequest = new ActivateUserRequest(); + activateRequest.setActivateToken(getActivateToken(userId)); + activateRequest.setPassword(password); + return client.activateUser(activateRequest, sendActivationMail); + } + + protected String getActivateToken(UserId userId) throws ApiException { + String activateTokenRegex = "/api/noauth/activate?activateToken="; + String activationLink = client.getActivationLink(userId.getId().toString()); + return activationLink.substring(activationLink.lastIndexOf(activateTokenRegex) + activateTokenRegex.length()); + } + + protected void assertReturns404(ThrowingRunnable operation) { + try { + operation.run(); + fail("Expected ApiException with 404 status code"); + } catch (ApiException exception) { + assertEquals("Expected 404 status code but got " + exception.getCode(), + 404, exception.getCode()); + } catch (Exception e) { + fail("Expected ApiException but got " + e.getClass().getName() + ": " + e.getMessage()); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java new file mode 100644 index 0000000000..4c6b3444b0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.client.model.AdminSettings; +import org.thingsboard.client.model.FeaturesInfo; +import org.thingsboard.client.model.JwtSettings; +import org.thingsboard.client.model.SecuritySettings; +import org.thingsboard.client.model.SystemInfo; +import org.thingsboard.client.model.UpdateMessage; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AdminJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testAdminSettingsLifecycle() throws Exception { + // authenticate as sysadmin for admin settings management + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // get mail settings + AdminSettings mailSettings = client.getAdminSettings("mail"); + assertNotNull(mailSettings); + assertNotNull(mailSettings.getKey()); + assertEquals("mail", mailSettings.getKey()); + assertNotNull(mailSettings.getJsonValue()); + + // get general settings + AdminSettings generalSettings = client.getAdminSettings("general"); + assertNotNull(generalSettings); + assertEquals("general", generalSettings.getKey()); + assertNotNull(generalSettings.getJsonValue()); + assertNotNull(generalSettings.getJsonValue().get("baseUrl").asText()); + + // update general settings and restore + ((ObjectNode) generalSettings.getJsonValue()).put("prohibitDifferentUrl", true); + AdminSettings updatedGeneralSettings = client.saveAdminSettings(generalSettings); + assertTrue(updatedGeneralSettings.getJsonValue().get("prohibitDifferentUrl").asBoolean()); + + // get security settings + SecuritySettings securitySettings = client.getSecuritySettings(); + assertNotNull(securitySettings); + assertNotNull(securitySettings.getPasswordPolicy()); + Integer originalMaxAttempts = securitySettings.getMaxFailedLoginAttempts(); + + // update security settings + securitySettings.setMaxFailedLoginAttempts(10); + SecuritySettings updatedSecurity = client.saveSecuritySettings(securitySettings); + assertNotNull(updatedSecurity); + assertEquals(10, updatedSecurity.getMaxFailedLoginAttempts().intValue()); + + // restore original security settings + updatedSecurity.setMaxFailedLoginAttempts(originalMaxAttempts); + client.saveSecuritySettings(updatedSecurity); + + // get JWT settings + JwtSettings jwtSettings = client.getJwtSettings(); + assertNotNull(jwtSettings); + assertNotNull(jwtSettings.getTokenExpirationTime()); + assertNotNull(jwtSettings.getRefreshTokenExpTime()); + assertEquals("thingsboard.io", jwtSettings.getTokenIssuer()); + assertNotNull(jwtSettings.getTokenSigningKey()); + + // get system info + SystemInfo systemInfo = client.getSystemInfo(); + assertNotNull(systemInfo); + + // get features info + FeaturesInfo featuresInfo = client.getFeaturesInfo(); + assertNotNull(featuresInfo); + assertFalse(featuresInfo.getSmsEnabled()); + assertTrue(featuresInfo.getOauthEnabled()); + + // check updates + UpdateMessage updateMessage = client.checkUpdates(); + assertNotNull(updateMessage); + assertNotNull(updateMessage.getCurrentVersion()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java new file mode 100644 index 0000000000..6b519d5fd0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.client.model.Alarm; +import org.thingsboard.client.model.AlarmComment; +import org.thingsboard.client.model.AlarmCommentInfo; +import org.thingsboard.client.model.AlarmSeverity; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.PageDataAlarmCommentInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class AlarmCommentJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testAlarmComments() throws Exception { + long timestamp = System.currentTimeMillis(); + + // Create device for alarm + Device device = new Device(); + device.setName("Device_For_Comments_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + + // Create alarm + Alarm alarm = new Alarm(); + alarm.setType("Temperature Alarm"); + alarm.setSeverity(AlarmSeverity.CRITICAL); + alarm.setOriginator(createdDevice.getId()); + + Alarm createdAlarm = client.saveAlarm(alarm); + String alarmId = createdAlarm.getId().getId().toString(); + + List createdComments = new ArrayList<>(); + + // Create multiple comments + for (int i = 0; i < 5; i++) { + AlarmComment alarmComment = new AlarmComment(); + String message = "Test comment #" + i + " at " + timestamp; + ObjectNode comment = OBJECT_MAPPER.createObjectNode().put("message", message); + alarmComment.setComment(comment); + + AlarmComment commentInfo = client.saveAlarmComment(alarmId, alarmComment); + + assertNotNull(commentInfo); + assertNotNull(commentInfo.getId()); + JsonNode commentValue = commentInfo.getComment(); + assertEquals(message, commentValue.get("message").asText()); + assertNotNull(commentInfo.getCreatedTime()); + + createdComments.add(commentInfo); + } + + // Get all comments for the alarm + PageDataAlarmCommentInfo allComments = client.getAlarmComments(alarmId, 100, 0, null, null); + assertEquals("Expected 5 comments", 5, allComments.getData().size()); + + // Update a comment + AlarmComment commentToUpdate = createdComments.get(2); + JsonNode comment = commentToUpdate.getComment(); + ((ObjectNode) comment).put("message", "New comment"); + commentToUpdate.setComment(comment); + + AlarmComment updatedComment = client.saveAlarmComment(alarmId, commentToUpdate); + assertEquals("New comment", updatedComment.getComment().get("message").asText()); + + // Delete a comment + UUID commentToDeleteId = createdComments.get(0).getId().getId(); + + client.deleteAlarmComment(alarmId, commentToDeleteId.toString()); + + // Verify comment was updated to "deleted" + PageDataAlarmCommentInfo commentsAfterDelete = client.getAlarmComments(alarmId, 100, 0, null, null); + List data = commentsAfterDelete.getData(); + AlarmCommentInfo deletedComment = data.stream() + .filter(alarmCommentInfo -> alarmCommentInfo.getId().getId().equals(commentToDeleteId)) + .findFirst() + .get(); + assertEquals("User " + clientTenantAdmin.getEmail() + " deleted his comment", deletedComment.getComment().get("text").asText()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java new file mode 100644 index 0000000000..61666297dc --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Alarm; +import org.thingsboard.client.model.AlarmInfo; +import org.thingsboard.client.model.AlarmSeverity; +import org.thingsboard.client.model.AlarmStatus; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntitySubtype; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.PageDataAlarmInfo; +import org.thingsboard.client.model.PageDataEntitySubtype; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AlarmJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testAlarmLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdAlarms = new ArrayList<>(); + + // First, create devices to attach alarms to + Device device1 = new Device(); + device1.setName("Device_For_Alarm_" + timestamp + "_1"); + device1.setType("default"); + Device createdDevice1 = client.saveDevice(device1, null, null, null, null); + + Device device2 = new Device(); + device2.setName("Device_For_Alarm_" + timestamp + "_2"); + device2.setType("thermostat"); + Device createdDevice2 = client.saveDevice(device2, null, null, null, null); + + // Create 2 alarms (1 for each device) + for (int i = 0; i < 2; i++) { + Alarm alarm = new Alarm(); + alarm.setType(((i % 2 == 0) ? "Temperature Alarm" : "Connection Alarm")); + alarm.setSeverity(((i % 2 == 0) ? AlarmSeverity.CRITICAL : AlarmSeverity.WARNING)); + alarm.setOriginator((i % 2 == 0) ? createdDevice1.getId() : createdDevice2.getId()); + + Alarm createdAlarm = client.saveAlarm(alarm); + assertNotNull(createdAlarm); + assertNotNull(createdAlarm.getId()); + assertEquals(alarm.getType(), createdAlarm.getType()); + assertEquals(alarm.getSeverity(), createdAlarm.getSeverity()); + + createdAlarms.add(createdAlarm); + } + + // Get all alarms + PageDataAlarmInfo allAlarms = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null); + + assertNotNull(allAlarms); + assertNotNull(allAlarms.getData()); + int initialSize = allAlarms.getData().size(); + assertEquals("Expected at least 2 alarms, but got " + initialSize, 2, initialSize); + + // Get alarms by entity (device1) + PageDataAlarmInfo device1Alarms = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null); + assertNotNull(device1Alarms); + assertEquals("Expected 1 alarms for device1", 1, device1Alarms.getData().size()); + + // Get alarm by id + Alarm searchAlarm = createdAlarms.get(0); + Alarm fetchedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString()); + assertEquals(searchAlarm.getType(), fetchedAlarm.getType()); + assertEquals(searchAlarm.getSeverity(), fetchedAlarm.getSeverity()); + + // Get alarm info + AlarmInfo alarmInfo = client.getAlarmInfoById(searchAlarm.getId().getId().toString()); + assertNotNull(alarmInfo); + assertEquals(searchAlarm.getId().getId(), alarmInfo.getId().getId()); + + // Acknowledge alarm + client.ackAlarm(searchAlarm.getId().getId().toString()); + + // Verify alarm is acknowledged + Alarm ackedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString()); + assertEquals(AlarmStatus.ACTIVE_ACK, ackedAlarm.getStatus()); + + // Clear alarm + client.clearAlarm(searchAlarm.getId().getId().toString()); + + // Verify alarm is cleared + Alarm clearedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString()); + assertEquals(AlarmStatus.CLEARED_ACK, clearedAlarm.getStatus()); + + // Get highest severity alarm for device + AlarmSeverity highestSeverity = client.getHighestAlarmSeverity(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), null, null, null); + assertNotNull(highestSeverity); + assertEquals(AlarmSeverity.CRITICAL, highestSeverity); + + // Assign alarm to customer + client.assignAlarm(createdAlarms.get(0).getId().getId().toString(), clientTenantAdmin.getId().getId().toString()); + + // Verify assignment + Alarm assignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString()); + assertEquals(clientTenantAdmin.getId().getId(), assignedAlarm.getAssigneeId().getId()); + + // Unassign alarm + client.unassignAlarm(createdAlarms.get(0).getId().getId().toString()); + + // Verify unassignment + Alarm unassignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString()); + assertNull(unassignedAlarm.getAssigneeId()); + + // Get alarm types + PageDataEntitySubtype pageDataEntitySubtype = client.getAlarmTypes(100, 0, null, null); + assertEquals(2, pageDataEntitySubtype.getData().size()); + List alarmTypes = pageDataEntitySubtype.getData().stream() + .map(EntitySubtype::getType) + .collect(Collectors.toList()); + assertTrue(alarmTypes.containsAll(List.of("Temperature Alarm", "Connection Alarm"))); + + // Get alarms V2 (alternative endpoint) + PageDataAlarmInfo alarmsV2 = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null); + assertNotNull(alarmsV2); + assertEquals(1, alarmsV2.getData().size()); + + // Get all alarms V2 + PageDataAlarmInfo allAlarmsV2 = client.getAllAlarmsV2(100, 0, null, null, null, null, null, null, null, null, null); + assertEquals(2, allAlarmsV2.getData().size()); + + // Delete alarm + UUID alarmToDeleteId = createdAlarms.get(0).getId().getId(); + client.deleteAlarm(alarmToDeleteId.toString()); + + // Verify the alarm is deleted (should return 404) + assertReturns404(() -> + client.getAlarmById(alarmToDeleteId.toString()) + ); + + // Verify count after deletion + PageDataAlarmInfo alarmsAfterDelete = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null); + assertEquals(initialSize - 1, alarmsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java new file mode 100644 index 0000000000..27cde622a4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Asset; +import org.thingsboard.client.model.PageDataAsset; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class AssetJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testAssetLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdAssets = new ArrayList<>(); + + // create 20 assets + for (int i = 0; i < 20; i++) { + Asset asset = new Asset(); + String assetName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + asset.setName(assetName); + asset.setLabel("Test Asset " + i); + asset.setType(((i % 2 == 0) ? "default" : "building")); + + Asset createdAsset = client.saveAsset(asset, null, null, null); + assertNotNull(createdAsset); + assertNotNull(createdAsset.getId()); + assertEquals(assetName, createdAsset.getName()); + + createdAssets.add(createdAsset); + } + + // find all, check count + PageDataAsset allAssets = client.getTenantAssets(100, 0, null, null, null, null); + + assertNotNull(allAssets); + assertNotNull(allAssets.getData()); + int initialSize = allAssets.getData().size(); + assertEquals("Expected at least 20 assets, but got " + allAssets.getData().size(), 20, initialSize); + + //find all with search text, check count + PageDataAsset allAssetsBySearchText = client.getTenantAssets(100, 0, null, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 test assets", 10, allAssetsBySearchText.getData().size()); + + // find by id + Asset searchAsset = createdAssets.get(10); + Asset asset = client.getAssetById(searchAsset.getId().getId().toString()); + assertEquals(searchAsset.getName(), asset.getName()); + + // delete asset + UUID assetToDeleteId = createdAssets.get(0).getId().getId(); + client.deleteAsset(assetToDeleteId.toString()); + + // Verify the asset is deleted + PageDataAsset assetsAfterDelete = client.getTenantAssets(100, 0, null, null, null, null); + assertEquals(initialSize - 1, assetsAfterDelete.getData().size()); + + assertReturns404(() -> + client.getAssetById(assetToDeleteId.toString()) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java new file mode 100644 index 0000000000..7825a047bd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AssetProfile; +import org.thingsboard.client.model.AssetProfileInfo; +import org.thingsboard.client.model.EntityInfo; +import org.thingsboard.client.model.PageDataAssetProfile; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AssetProfileJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testAssetProfileLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdProfiles = new ArrayList<>(); + + // Get initial count (there should be a default profile) + PageDataAssetProfile initialProfiles = client.getAssetProfiles(100, 0, null, null, null); + assertNotNull(initialProfiles); + int initialSize = initialProfiles.getData().size(); + assertTrue("Expected at least 1 default asset profile", initialSize == 1); + + // Get default asset profile info + AssetProfileInfo defaultProfileInfo = client.getDefaultAssetProfileInfo(); + assertNotNull(defaultProfileInfo); + assertEquals(defaultProfileInfo.getName(), "default"); + + // Create multiple asset profiles + for (int i = 0; i < 5; i++) { + AssetProfile profile = new AssetProfile(); + profile.setName("Test Asset Profile " + timestamp + "_" + i); + profile.setDescription("Test description " + i); + + AssetProfile created = client.saveAssetProfile(profile); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(profile.getName(), created.getName()); + assertEquals(profile.getDescription(), created.getDescription()); + assertFalse(created.getDefault()); + + createdProfiles.add(created); + } + + // Find all, check count + PageDataAssetProfile allProfiles = client.getAssetProfiles(100, 0, null, null, null); + assertNotNull(allProfiles); + assertEquals(initialSize + 5, allProfiles.getData().size()); + + // Find all with text search + PageDataAssetProfile filteredProfiles = client.getAssetProfiles(100, 0, "Test Asset Profile " + timestamp, null, null); + assertEquals(5, filteredProfiles.getData().size()); + + // Get by id + AssetProfile searchProfile = createdProfiles.get(2); + AssetProfile fetchedProfile = client.getAssetProfileById(searchProfile.getId().getId().toString(), false); + assertEquals(searchProfile.getName(), fetchedProfile.getName()); + assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription()); + + // Update asset profile + fetchedProfile.setDescription("Updated description"); + AssetProfile updatedProfile = client.saveAssetProfile(fetchedProfile); + assertEquals("Updated description", updatedProfile.getDescription()); + assertEquals(fetchedProfile.getName(), updatedProfile.getName()); + + // Get asset profile info by id + AssetProfileInfo profileInfo = client.getAssetProfileInfoById(searchProfile.getId().getId().toString()); + assertNotNull(profileInfo); + assertEquals(searchProfile.getName(), profileInfo.getName()); + + // Get asset profile infos (paginated) + PageDataAssetProfile profileInfos = client.getAssetProfiles(100, 0, null, null, null); + assertNotNull(profileInfos); + assertEquals(initialSize + 5, profileInfos.getData().size()); + + // Set a profile as default + AssetProfile profileToSetDefault = createdProfiles.get(1); + AssetProfile newDefault = client.setDefaultAssetProfile(profileToSetDefault.getId().getId().toString()); + assertNotNull(newDefault); + assertTrue(newDefault.getDefault()); + + // Verify default profile info now points to the new default + AssetProfileInfo newDefaultInfo = client.getDefaultAssetProfileInfo(); + assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName()); + + // Get asset profile names + List profileNames = client.getAssetProfileNames(false); + assertNotNull(profileNames); + assertEquals(createdProfiles.size() + 1, profileNames.size()); + + // Delete asset profile (cannot delete the default one, so delete a non-default one) + UUID profileToDeleteId = createdProfiles.get(0).getId().getId(); + client.deleteAssetProfile(profileToDeleteId.toString()); + + // Verify the profile is deleted + assertReturns404(() -> + client.getAssetProfileById(profileToDeleteId.toString(), false)); + + // Verify count after deletion + PageDataAssetProfile profilesAfterDelete = client.getAssetProfiles(100, 0, null, null, null); + assertEquals(initialSize + 4, profilesAfterDelete.getData().size()); + + // Restore original default profile + AssetProfile originalDefault = initialProfiles.getData().stream() + .filter(AssetProfile::getDefault) + .findFirst() + .orElseThrow(); + client.setDefaultAssetProfile(originalDefault.getId().getId().toString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java new file mode 100644 index 0000000000..6f79f0d347 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java @@ -0,0 +1,286 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AlarmCalculatedFieldConfiguration; +import org.thingsboard.client.model.AlarmConditionValueAlarmRuleSchedule; +import org.thingsboard.client.model.AlarmRuleDefinition; +import org.thingsboard.client.model.AlarmRuleSimpleCondition; +import org.thingsboard.client.model.AlarmRuleSpecificTimeSchedule; +import org.thingsboard.client.model.AlarmSeverity; +import org.thingsboard.client.model.Argument; +import org.thingsboard.client.model.ArgumentType; +import org.thingsboard.client.model.CalculatedField; +import org.thingsboard.client.model.CalculatedFieldType; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.PageDataCalculatedField; +import org.thingsboard.client.model.ReferencedEntityKey; +import org.thingsboard.client.model.SimpleCalculatedFieldConfiguration; +import org.thingsboard.client.model.TbelAlarmConditionExpression; +import org.thingsboard.client.model.TimeSeriesOutput; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class CalculatedFieldJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testCalculatedFieldLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdFields = new ArrayList<>(); + + // create devices to attach calculated fields to + Device device1 = new Device(); + device1.setName("CalcFieldDevice1_" + timestamp); + device1.setType("default"); + Device createdDevice1 = client.saveDevice(device1, null, null, null, null); + + Device device2 = new Device(); + device2.setName("CalcFieldDevice2_" + timestamp); + device2.setType("default"); + Device createdDevice2 = client.saveDevice(device2, null, null, null, null); + + // create calculated fields on device1 + for (int i = 0; i < 5; i++) { + CalculatedField cf = new CalculatedField(); + cf.setName(TEST_PREFIX + "CalcField_" + timestamp + "_" + i); + cf.setType(CalculatedFieldType.SIMPLE); + + cf.setEntityId(createdDevice1.getId()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument arg = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey(); + refKey.setKey("temperature"); + refKey.setType(ArgumentType.TS_LATEST); + arg.setRefEntityKey(refKey); + config.putArgumentsItem("temp", arg); + + config.setExpression("temp * " + (i + 1)); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("scaledTemp_" + i); + config.setOutput(output); + + cf.setConfiguration(config); + + CalculatedField created = client.saveCalculatedField(cf); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(cf.getName(), created.getName()); + assertEquals(CalculatedFieldType.SIMPLE, created.getType()); + + createdFields.add(created); + } + + // create calculated fields on device2 + for (int i = 0; i < 3; i++) { + CalculatedField cf = new CalculatedField(); + cf.setName(TEST_PREFIX + "CalcField2_" + timestamp + "_" + i); + cf.setType(CalculatedFieldType.SIMPLE); + cf.setEntityId(createdDevice2.getId()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument arg = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey(); + refKey.setKey("humidity"); + refKey.setType(ArgumentType.TS_LATEST); + arg.setRefEntityKey(refKey); + config.putArgumentsItem("hum", arg); + + config.setExpression("hum + " + i); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("adjustedHumidity_" + i); + config.setOutput(output); + + cf.setConfiguration(config); + + CalculatedField created = client.saveCalculatedField(cf); + assertNotNull(created); + createdFields.add(created); + } + + // get calculated fields by entity id for device1 + PageDataCalculatedField device1Fields = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), + 100, 0, CalculatedFieldType.SIMPLE, null, null, null); + assertNotNull(device1Fields); + assertEquals(5, device1Fields.getData().size()); + + // get calculated fields by entity id for device2 + PageDataCalculatedField device2Fields = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice2.getId().getId().toString(), + 100, 0, CalculatedFieldType.SIMPLE, null, null, null); + assertEquals(3, device2Fields.getData().size()); + + // get by id + CalculatedField searchField = createdFields.get(2); + CalculatedField fetchedField = client.getCalculatedFieldById(searchField.getId().getId().toString()); + assertEquals(searchField.getName(), fetchedField.getName()); + assertEquals(searchField.getType(), fetchedField.getType()); + assertNotNull(fetchedField.getConfiguration()); + SimpleCalculatedFieldConfiguration fetchedConfig = + (SimpleCalculatedFieldConfiguration) fetchedField.getConfiguration(); + assertEquals("temp * 3", fetchedConfig.getExpression()); + + // update calculated field + fetchedField.setName(fetchedField.getName() + "_updated"); + fetchedConfig.setExpression("temp * 100"); + CalculatedField updatedField = client.saveCalculatedField(fetchedField); + assertEquals(fetchedField.getName(), updatedField.getName()); + SimpleCalculatedFieldConfiguration updatedConfig = + (SimpleCalculatedFieldConfiguration) updatedField.getConfiguration(); + assertEquals("temp * 100", updatedConfig.getExpression()); + + // delete calculated field + UUID fieldToDeleteId = createdFields.get(0).getId().getId(); + client.deleteCalculatedField(fieldToDeleteId.toString()); + + // verify deletion + assertReturns404(() -> + client.getCalculatedFieldById(fieldToDeleteId.toString()) + ); + + PageDataCalculatedField device1FieldsAfterDelete = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), + 100, 0, null, null, null, null); + assertEquals(4, device1FieldsAfterDelete.getData().size()); + } + + @Test + public void testAlarmCalculatedFieldLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create a device to attach the alarm calculated field to + Device device = new Device(); + device.setName("AlarmCalcFieldDevice_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + + // build the alarm calculated field configuration + AlarmCalculatedFieldConfiguration config = new AlarmCalculatedFieldConfiguration(); + + // argument: temperature time-series + Argument tempArg = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey(); + refKey.setKey("temperature"); + refKey.setType(ArgumentType.TS_LATEST); + tempArg.setRefEntityKey(refKey); + config.putArgumentsItem("temp", tempArg); + + // create rule: HIGH_TEMPERATURE when temp > 50 (TBEL expression) + TbelAlarmConditionExpression createExpression = new TbelAlarmConditionExpression(); + createExpression.setExpression("return temp > 50;"); + AlarmRuleSimpleCondition createCondition = new AlarmRuleSimpleCondition(); + createCondition.setExpression(createExpression); + AlarmRuleSpecificTimeSchedule specificTimeSchedule = new AlarmRuleSpecificTimeSchedule().addDaysOfWeekItem(3); + AlarmConditionValueAlarmRuleSchedule schedule = new AlarmConditionValueAlarmRuleSchedule().staticValue(specificTimeSchedule); + createCondition.setSchedule(schedule); + AlarmRuleDefinition createRule = new AlarmRuleDefinition(); + createRule.setCondition(createCondition); + createRule.setAlarmDetails("Temperature is too high: ${temp}"); + config.setCreateRules(Map.of( + AlarmSeverity.CRITICAL.name(), createRule + )); + + // clear rule: when temp drops below 30 + TbelAlarmConditionExpression clearExpression = new TbelAlarmConditionExpression(); + clearExpression.setExpression("return temp < 30;"); + AlarmRuleSimpleCondition clearCondition = new AlarmRuleSimpleCondition(); + clearCondition.setExpression(clearExpression); + AlarmRuleDefinition clearRule = new AlarmRuleDefinition(); + clearRule.setCondition(clearCondition); + config.setClearRule(clearRule); + + config.setPropagate(true); + config.setPropagateToOwner(false); + + // create calculated field + CalculatedField cf = new CalculatedField(); + cf.setName(TEST_PREFIX + "AlarmCalcField_" + timestamp); + cf.setType(CalculatedFieldType.ALARM); + + cf.setEntityId(createdDevice.getId()); + cf.setConfiguration(config); + + CalculatedField created = client.saveCalculatedField(cf); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(cf.getName(), created.getName()); + assertEquals(CalculatedFieldType.ALARM, created.getType()); + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) created.getConfiguration(); + AlarmConditionValueAlarmRuleSchedule createdSchedule = configuration.getCreateRules().get(AlarmSeverity.CRITICAL.name()).getCondition().getSchedule(); + AlarmRuleSpecificTimeSchedule staticSchedule = (AlarmRuleSpecificTimeSchedule) createdSchedule.getStaticValue(); + assertEquals(Set.of(3), staticSchedule.getDaysOfWeek()); + + // get by id and verify configuration + CalculatedField fetched = client.getCalculatedFieldById(created.getId().getId().toString()); + assertNotNull(fetched); + assertEquals(created.getName(), fetched.getName()); + assertEquals(CalculatedFieldType.ALARM, fetched.getType()); + assertNotNull(fetched.getConfiguration()); + AlarmCalculatedFieldConfiguration fetchedConfig = + (AlarmCalculatedFieldConfiguration) fetched.getConfiguration(); + assertNotNull(fetchedConfig.getCreateRules()); + assertEquals(1, fetchedConfig.getCreateRules().size()); + assertTrue(fetchedConfig.getCreateRules().containsKey("CRITICAL")); + assertNotNull(fetchedConfig.getClearRule()); + assertEquals(Boolean.TRUE, fetchedConfig.getPropagate()); + + // update: add a second create rule for CRITICAL_TEMPERATURE + TbelAlarmConditionExpression criticalExpression = new TbelAlarmConditionExpression(); + criticalExpression.setExpression("return temp > 80;"); + AlarmRuleSimpleCondition criticalCondition = new AlarmRuleSimpleCondition(); + criticalCondition.setExpression(criticalExpression); + AlarmRuleDefinition criticalRule = new AlarmRuleDefinition(); + criticalRule.setCondition(criticalCondition); + fetchedConfig.putCreateRulesItem(AlarmSeverity.INDETERMINATE.name(), criticalRule); + fetched.setConfiguration(fetchedConfig); + + CalculatedField updated = client.saveCalculatedField(fetched); + AlarmCalculatedFieldConfiguration updatedConfig = + (AlarmCalculatedFieldConfiguration) updated.getConfiguration(); + assertEquals(2, updatedConfig.getCreateRules().size()); + assertTrue(updatedConfig.getCreateRules().containsKey("INDETERMINATE")); + + // filter by entity and ALARM type + PageDataCalculatedField deviceFields = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice.getId().getId().toString(), + 100, 0, CalculatedFieldType.ALARM, null, null, null); + assertNotNull(deviceFields); + assertEquals(1, deviceFields.getData().size()); + + // delete and verify + UUID fieldId = created.getId().getId(); + client.deleteCalculatedField(fieldId.toString()); + assertReturns404(() -> client.getCalculatedFieldById(fieldId.toString())); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java new file mode 100644 index 0000000000..dbfe1faf56 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Customer; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.PageDataCustomer; +import org.thingsboard.client.model.PageDataDevice; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class CustomerJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testCustomerLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdCustomers = new ArrayList<>(); + + // create 20 customers + for (int i = 0; i < 20; i++) { + Customer customer = new Customer(); + String customerTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + customer.setTitle(customerTitle); + customer.setEmail("customer_" + timestamp + "_" + i + "@test.com"); + + Customer createdCustomer = client.saveCustomer(customer, null, null, null); + assertNotNull(createdCustomer); + assertNotNull(createdCustomer.getId()); + assertEquals(customerTitle, createdCustomer.getTitle()); + + createdCustomers.add(createdCustomer); + } + + // find all, check count (includes savedClientCustomer from AbstractJavaClientTest setup) + PageDataCustomer allCustomers = client.getCustomers(100, 0, null, null, null); + assertNotNull(allCustomers); + assertNotNull(allCustomers.getData()); + int initialSize = allCustomers.getData().size(); + assertEquals("Expected 21 customers (20 created + 1 from setup), but got " + initialSize, 21, initialSize); + + // find all with search text, check count + PageDataCustomer filteredCustomers = client.getCustomers(100, 0, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 customers matching prefix", 10, filteredCustomers.getData().size()); + + // find by id + Customer searchCustomer = createdCustomers.get(10); + Customer fetchedCustomer = client.getCustomerById(searchCustomer.getId().getId().toString()); + assertEquals(searchCustomer.getTitle(), fetchedCustomer.getTitle()); + + // find by title + Customer fetchedByTitle = client.getTenantCustomer(searchCustomer.getTitle()); + assertEquals(searchCustomer.getId().getId(), fetchedByTitle.getId().getId()); + + // update customer + fetchedCustomer.setCity("New York"); + fetchedCustomer.setCountry("US"); + Customer updatedCustomer = client.saveCustomer(fetchedCustomer, null, null, null); + assertEquals("New York", updatedCustomer.getCity()); + assertEquals("US", updatedCustomer.getCountry()); + + // assign device to customer and verify + Device device = new Device(); + device.setName("CustomerTestDevice_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + + String customerId = createdCustomers.get(0).getId().getId().toString(); + client.assignDeviceToCustomer(customerId, createdDevice.getId().getId().toString()); + + PageDataDevice customerDevices = client.getCustomerDevices(customerId, 100, 0, null, null, null, null); + assertEquals(1, customerDevices.getData().size()); + assertEquals(createdDevice.getName(), customerDevices.getData().get(0).getName()); + + // unassign device from customer + client.unassignDeviceFromCustomer(createdDevice.getId().getId().toString()); + PageDataDevice devicesAfterUnassign = client.getCustomerDevices(customerId, 100, 0, null, null, null, null); + assertEquals(0, devicesAfterUnassign.getData().size()); + + // delete customer + UUID customerToDeleteId = createdCustomers.get(0).getId().getId(); + client.deleteCustomer(customerToDeleteId.toString()); + + // verify deletion + PageDataCustomer customersAfterDelete = client.getCustomers(100, 0, null, null, null); + assertEquals(initialSize - 1, customersAfterDelete.getData().size()); + + assertReturns404(() -> + client.getCustomerById(customerToDeleteId.toString()) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java new file mode 100644 index 0000000000..5d1fa871c8 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java @@ -0,0 +1,123 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Dashboard; +import org.thingsboard.client.model.DashboardInfo; +import org.thingsboard.client.model.PageDataDashboardInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class DashboardJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testDashboardLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create 20 dashboards + for (int i = 0; i < 20; i++) { + Dashboard dashboard = new Dashboard(); + String dashboardTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + dashboard.setTitle(dashboardTitle); + + client.saveDashboard(dashboard, null); + } + + // find all, check count + PageDataDashboardInfo allDashboards = client.getTenantDashboards(100, 0, null, null, null, null); + assertNotNull(allDashboards); + assertNotNull(allDashboards.getData()); + int initialSize = allDashboards.getData().size(); + assertEquals("Expected 20 dashboards, but got " + initialSize, 20, initialSize); + + List createdDashboards = allDashboards.getData(); + + // find all with search text, check count + PageDataDashboardInfo filteredDashboards = client.getTenantDashboards(100, 0, null, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 dashboards matching prefix", 10, filteredDashboards.getData().size()); + + // find by id + DashboardInfo searchDashboard = createdDashboards.get(10); + DashboardInfo fetchedDashboard = client.getDashboardInfoById(searchDashboard.getId().getId().toString()); + assertEquals(searchDashboard.getTitle(), fetchedDashboard.getTitle()); + + // update dashboard + Dashboard dashboardToUpdate = new Dashboard(); + dashboardToUpdate.setId(fetchedDashboard.getId()); + dashboardToUpdate.setTitle(fetchedDashboard.getTitle() + "_updated"); + dashboardToUpdate.setVersion(fetchedDashboard.getVersion()); + client.saveDashboard(dashboardToUpdate, null); + + DashboardInfo updatedDashboard = client.getDashboardInfoById(fetchedDashboard.getId().getId().toString()); + assertEquals(fetchedDashboard.getTitle() + "_updated", updatedDashboard.getTitle()); + + // assign dashboard to customer and verify + String customerId = savedClientCustomer.getId().getId().toString(); + String dashboardId = createdDashboards.get(0).getId().getId().toString(); + client.assignDashboardToCustomer(customerId, dashboardId); + + PageDataDashboardInfo customerDashboards = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null); + assertEquals(1, customerDashboards.getData().size()); + assertEquals(createdDashboards.get(0).getTitle(), customerDashboards.getData().get(0).getTitle()); + + // unassign dashboard from customer + client.unassignDashboardFromCustomer(customerId, dashboardId); + PageDataDashboardInfo dashboardsAfterUnassign = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null); + assertEquals(0, dashboardsAfterUnassign.getData().size()); + + // make dashboard public and verify + client.assignDashboardToPublicCustomer(dashboardId); + DashboardInfo publicDashboard = client.getDashboardInfoById(dashboardId); + assertNotNull(publicDashboard.getAssignedCustomers()); + assertTrue(publicDashboard.getAssignedCustomers().size() > 0); + + // remove public access + client.unassignDashboardFromPublicCustomer(dashboardId); + + // delete dashboard + UUID dashboardToDeleteId = createdDashboards.get(0).getId().getId(); + client.deleteDashboard(dashboardToDeleteId.toString()); + + // verify deletion + PageDataDashboardInfo dashboardsAfterDelete = client.getTenantDashboards(100, 0, null, null, null, null); + assertEquals(initialSize - 1, dashboardsAfterDelete.getData().size()); + + assertReturns404(() -> + client.getDashboardInfoById(dashboardToDeleteId.toString()) + ); + } + + @Test + public void testGetServerTime() throws Exception { + Long serverTime = client.getServerTime(); + assertNotNull(serverTime); + } + + @Test + public void testGetMaxDatapointsLimit() throws Exception { + Long maxDatapointsLimit = client.getMaxDatapointsLimit(); + assertNotNull(maxDatapointsLimit); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java new file mode 100644 index 0000000000..2ca2d622e7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; +import org.thingsboard.client.model.Device; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; + +@DaoSqlTest +public class DeviceConnectivityJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testGetDevicePublishTelemetryCommands() throws Exception { + Device device = new Device(); + device.setName(TEST_PREFIX + System.currentTimeMillis()); + device.setType("default"); + + Device savedDevice = client.saveDevice(device, null, null, null, null); + String token = client.getDeviceCredentialsByDeviceId(savedDevice.getId().getId().toString()).getCredentialsId(); + + String deviceId = savedDevice.getId().getId().toString(); + + JsonNode commands = client.getDevicePublishTelemetryCommands(deviceId); + assertEquals("curl -v -X POST http://localhost:8080/api/v1/" + token + "/telemetry --header Content-Type:application/json --data \"{temperature:25}\"", commands.get("http").get("http").asText()); + assertEquals("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry -u \"" + token + "\" -m \"{temperature:25}\"", commands.get("mqtt").get("mqtt").asText()); + assertEquals("coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://localhost:5683/api/v1/" + token + "/telemetry", commands.get("coap").get("coap").asText()); + } + + @Test + public void testGetDevicePublishTelemetryCommands_nonExistentDevice() { + String nonExistentId = UUID.randomUUID().toString(); + assertReturns404(() -> client.getDevicePublishTelemetryCommands(nonExistentId)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java new file mode 100644 index 0000000000..c141565c42 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.DeviceCredentials; +import org.thingsboard.client.model.DeviceCredentialsType; +import org.thingsboard.client.model.PageDataDevice; +import org.thingsboard.client.model.SaveDeviceWithCredentialsRequest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class DeviceJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testDeviceLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdDevices = new ArrayList<>(); + + // create 20 devices + for (int i = 0; i < 20; i++) { + Device device = new Device(); + String deviceName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + device.setName(deviceName); + device.setLabel("Test Device " + i); + device.setType(((i % 2 == 0) ? "default" : "thermostat")); + + Device createdDevice = client.saveDevice(device, null, null, null, null); + assertNotNull(createdDevice); + assertNotNull(createdDevice.getId()); + assertEquals(deviceName, createdDevice.getName()); + + createdDevices.add(createdDevice); + } + + // find all, check count + PageDataDevice allDevices = client.getTenantDevices(100, 0, null, null, null, null); + + assertNotNull(allDevices); + assertNotNull(allDevices.getData()); + int initialSize = allDevices.getData().size(); + assertEquals("Expected at least 20 devices, but got " + allDevices.getData().size(), 20, initialSize); + + //find all with search text, check count + PageDataDevice allDevicesBySearchText = client.getTenantDevices(10, 0, null, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 test devices", 10, allDevicesBySearchText.getData().size()); + + // find by id + Device searchDevice = createdDevices.get(10); + Device device = client.getDeviceById(searchDevice.getId().getId().toString()); + assertEquals(searchDevice.getName(), device.getName()); + + // create device with credentials + Device deviceWithCreds = new Device(); + deviceWithCreds.setName("device-with-creds"); + + DeviceCredentials creds = new DeviceCredentials(); + creds.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + creds.setCredentialsId("TEST_ACCESS_TOKEN"); + + SaveDeviceWithCredentialsRequest request = new SaveDeviceWithCredentialsRequest(); + request.setDevice(deviceWithCreds); + request.setCredentials(creds); + + Device savedDeviceWithCreds = client.saveDeviceWithCredentials(request, null, null, null); + assertEquals("device-with-creds", savedDeviceWithCreds.getName()); + + // find credentials by device id + DeviceCredentials fetchedCreds = client.getDeviceCredentialsByDeviceId(savedDeviceWithCreds.getId().getId().toString()); + assertEquals(creds.getCredentialsId(), fetchedCreds.getCredentialsId()); + + // delete device + UUID deviceToDeleteId = createdDevices.get(0).getId().getId(); + client.deleteDevice(deviceToDeleteId.toString()); + + // Verify the device is deleted + PageDataDevice devicesAfterDelete = client.getTenantDevices(100, 0, null, null, null, null); + assertEquals(initialSize, devicesAfterDelete.getData().size()); + + assertReturns404(() -> + client.getDeviceById(deviceToDeleteId.toString())); + + // assign device to customer + client.assignDeviceToCustomer(savedClientCustomer.getId().getId().toString(), savedDeviceWithCreds.getId().getId().toString()); + + // check customer devices + PageDataDevice pageDataDevice = client.getCustomerDevices(savedClientCustomer.getId().getId().toString(), 100, 0, null, null, null, null); + List data = pageDataDevice.getData(); + assertEquals(1, data.size()); + assertEquals(savedDeviceWithCreds.getName(), data.get(0).getName()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java new file mode 100644 index 0000000000..6008c85653 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java @@ -0,0 +1,157 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.DefaultDeviceProfileConfiguration; +import org.thingsboard.client.model.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.client.model.DeviceProfile; +import org.thingsboard.client.model.DeviceProfileData; +import org.thingsboard.client.model.DeviceProfileInfo; +import org.thingsboard.client.model.DeviceProfileType; +import org.thingsboard.client.model.DeviceTransportType; +import org.thingsboard.client.model.EntityInfo; +import org.thingsboard.client.model.PageDataDeviceProfile; +import org.thingsboard.client.model.PageDataDeviceProfileInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class DeviceProfileJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testDeviceProfileLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdProfiles = new ArrayList<>(); + + // Get initial count (there should be a default profile) + PageDataDeviceProfile initialProfiles = client.getDeviceProfiles(100, 0, null, null, null); + assertNotNull(initialProfiles); + int initialSize = initialProfiles.getData().size(); + assertTrue("Expected at least 1 default device profile", initialSize >= 1); + + // Get default device profile info + DeviceProfileInfo defaultProfileInfo = client.getDefaultDeviceProfileInfo(); + assertNotNull(defaultProfileInfo); + assertNotNull(defaultProfileInfo.getName()); + + // Create multiple device profiles + for (int i = 0; i < 5; i++) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName("Test Device Profile " + timestamp + "_" + i); + deviceProfile.setDescription("Test description " + i); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + configuration.setType(DeviceProfileType.DEFAULT.getValue()); + deviceProfileData.setConfiguration(configuration); + DefaultDeviceProfileTransportConfiguration transportConf = new DefaultDeviceProfileTransportConfiguration(); + transportConf.setType(DeviceTransportType.DEFAULT.getValue()); + deviceProfileData.setTransportConfiguration(transportConf); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setDefault(false); + deviceProfile.setDefaultRuleChainId(null); + + DeviceProfile created = client.saveDeviceProfile(deviceProfile); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(deviceProfile.getName(), created.getName()); + assertEquals(deviceProfile.getDescription(), created.getDescription()); + assertEquals(DeviceProfileType.DEFAULT, created.getType()); + assertEquals(DeviceTransportType.DEFAULT, created.getTransportType()); + assertFalse(created.getDefault()); + + createdProfiles.add(created); + } + + // Find all, check count + PageDataDeviceProfile allProfiles = client.getDeviceProfiles(100, 0, null, null, null); + assertNotNull(allProfiles); + assertEquals(initialSize + 5, allProfiles.getData().size()); + + // Find all with text search + PageDataDeviceProfile filteredProfiles = client.getDeviceProfiles(100, 0, "Test Device Profile " + timestamp, null, null); + assertEquals(5, filteredProfiles.getData().size()); + + // Get by id + DeviceProfile searchProfile = createdProfiles.get(2); + DeviceProfile fetchedProfile = client.getDeviceProfileById(searchProfile.getId().getId().toString(), false); + assertEquals(searchProfile.getName(), fetchedProfile.getName()); + assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription()); + + // Update device profile + fetchedProfile.setDescription("Updated description"); + DeviceProfile updatedProfile = client.saveDeviceProfile(fetchedProfile); + assertEquals("Updated description", updatedProfile.getDescription()); + assertEquals(fetchedProfile.getName(), updatedProfile.getName()); + + // Get device profile info by id + DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo(); + assertNotNull(profileInfo); + assertEquals(searchProfile.getType().getValue().toLowerCase(), profileInfo.getName()); + assertEquals(DeviceTransportType.DEFAULT, profileInfo.getTransportType()); + + // Get device profile infos (paginated) + PageDataDeviceProfileInfo profileInfos = client.getDeviceProfileInfos(100, 0, null, null, null, null); + assertNotNull(profileInfos); + assertEquals(initialSize + 5, profileInfos.getData().size()); + + // Set a profile as default + DeviceProfile profileToSetDefault = createdProfiles.get(1); + DeviceProfile newDefault = client.setDefaultDeviceProfile(profileToSetDefault.getId().getId().toString()); + assertNotNull(newDefault); + assertTrue(newDefault.getDefault()); + + // Verify default profile info now points to the new default + DeviceProfileInfo newDefaultInfo = client.getDefaultDeviceProfileInfo(); + assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName()); + + // Get device profile names + List profileNames = client.getDeviceProfileNames(false); + assertNotNull(profileNames); + assertEquals(createdProfiles.size() + 1, profileNames.size()); + + // Delete device profile (cannot delete the default one, so delete a non-default one) + UUID profileToDeleteId = createdProfiles.get(0).getId().getId(); + client.deleteDeviceProfile(profileToDeleteId.toString()); + + // Verify the profile is deleted + assertReturns404(() -> + client.getDeviceProfileById(profileToDeleteId.toString(), false)); + + // Verify count after deletion + PageDataDeviceProfile profilesAfterDelete = client.getDeviceProfiles(100, 0, null, null, null); + assertEquals(initialSize + 4, profilesAfterDelete.getData().size()); + + // Restore original default profile + DeviceProfile originalDefault = initialProfiles.getData().stream() + .filter(DeviceProfile::getDefault) + .findFirst() + .orElseThrow(); + client.setDefaultDeviceProfile(originalDefault.getId().getId().toString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java new file mode 100644 index 0000000000..fb8bb9b44f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.After; +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Domain; +import org.thingsboard.client.model.DomainInfo; +import org.thingsboard.client.model.PageDataDomainInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class DomainJavaClientTest extends AbstractJavaClientTest { + + List createdDomains = new ArrayList<>(); + + @After + public void afterDomainTest() { + createdDomains.forEach(domain -> { + try { + client.deleteDomain(domain.getId().getId()); + } catch (ApiException e) { + // ignore + } + }); + } + + @Test + public void testDomainLifecycle() throws Exception { + client.login("sysadmin@thingsboard.org", "sysadmin"); + + long timestamp = System.currentTimeMillis(); + + // create 5 domains + for (int i = 0; i < 5; i++) { + Domain domain = new Domain(); + domain.setName("domain." + i + ".com"); + domain.setOauth2Enabled(false); + domain.setPropagateToEdge(false); + + Domain created = client.saveDomain(domain, null); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(domain.getName(), created.getName()); + assertEquals(false, created.getOauth2Enabled()); + + createdDomains.add(created); + } + + // list tenant domains with text search + PageDataDomainInfo filteredDomains = client.getTenantDomainInfos(100, 0, + "domain.", null, null); + assertNotNull(filteredDomains); + assertEquals(5, filteredDomains.getData().size()); + + // get domain info by id + Domain searchDomain = createdDomains.get(2); + DomainInfo fetchedInfo = client.getDomainInfoById(searchDomain.getId().getId()); + assertEquals(searchDomain.getName(), fetchedInfo.getName()); + assertEquals(searchDomain.getOauth2Enabled(), fetchedInfo.getOauth2Enabled()); + assertNotNull(fetchedInfo.getOauth2ClientInfos()); + + // update domain + Domain domainToUpdate = createdDomains.get(3); + domainToUpdate.setPropagateToEdge(true); + Domain updatedDomain = client.saveDomain(domainToUpdate, null); + assertEquals(true, updatedDomain.getPropagateToEdge()); + + // delete domain + UUID domainToDeleteId = createdDomains.get(0).getId().getId(); + createdDomains.remove(0); + client.deleteDomain(domainToDeleteId); + + // verify deletion + assertReturns404(() -> + client.getDomainInfoById(domainToDeleteId) + ); + + PageDataDomainInfo domainsAfterDelete = client.getTenantDomainInfos(100, 0, + "domain.", null, null); + assertEquals(4, domainsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java new file mode 100644 index 0000000000..1400ead0b1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java @@ -0,0 +1,141 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Edge; +import org.thingsboard.client.model.EdgeInfo; +import org.thingsboard.client.model.PageDataEdge; +import org.thingsboard.client.model.PageDataEdgeInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class EdgeJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testEdgeLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdEdges = new ArrayList<>(); + + // create 5 edges + for (int i = 0; i < 5; i++) { + Edge edge = new Edge(); + edge.setName(TEST_PREFIX + "Edge_" + timestamp + "_" + i); + edge.setType("gateway"); + edge.setLabel("Test Edge " + i); + edge.setRoutingKey("routing_key_" + timestamp + "_" + i); + edge.setSecret("secret_key_" + timestamp + "_" + i); + + Edge created = client.saveEdge(edge); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(edge.getName(), created.getName()); + assertEquals("gateway", created.getType()); + assertNotNull(created.getRoutingKey()); + assertNotNull(created.getSecret()); + + createdEdges.add(created); + } + + // list tenant edges with text search + PageDataEdge filteredEdges = client.getTenantEdges(100, 0, null, + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertNotNull(filteredEdges); + assertEquals(5, filteredEdges.getData().size()); + + // list tenant edges with type filter + PageDataEdge typedEdges = client.getTenantEdges(100, 0, "gateway", + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(5, typedEdges.getData().size()); + + // get tenant edge infos + PageDataEdgeInfo edgeInfos = client.getTenantEdgeInfos(100, 0, null, + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(5, edgeInfos.getData().size()); + + // get edge by id + Edge searchEdge = createdEdges.get(2); + Edge fetchedEdge = client.getEdgeById(searchEdge.getId().getId().toString()); + assertEquals(searchEdge.getName(), fetchedEdge.getName()); + assertEquals(searchEdge.getType(), fetchedEdge.getType()); + assertEquals(searchEdge.getRoutingKey(), fetchedEdge.getRoutingKey()); + + // get edge by name + Edge fetchedByName = client.getTenantEdgeByName(searchEdge.getName()); + assertEquals(searchEdge.getId().getId(), fetchedByName.getId().getId()); + + // get edges by list of ids + List idsToFetch = List.of( + createdEdges.get(0).getId().getId().toString(), + createdEdges.get(1).getId().getId().toString() + ); + List edgeList = client.getEdgeList(idsToFetch); + assertEquals(2, edgeList.size()); + + // update edge + Edge edgeToUpdate = createdEdges.get(3); + edgeToUpdate.setLabel("Updated Label"); + Edge updatedEdge = client.saveEdge(edgeToUpdate); + assertEquals("Updated Label", updatedEdge.getLabel()); + + // assign edge to customer + String customerId = savedClientCustomer.getId().getId().toString(); + String edgeId = createdEdges.get(1).getId().getId().toString(); + Edge assignedEdge = client.assignEdgeToCustomer(customerId, edgeId); + assertNotNull(assignedEdge.getCustomerId()); + + // get customer edges + PageDataEdge customerEdges = client.getCustomerEdges(customerId, 100, 0, + null, TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(1, customerEdges.getData().size()); + + // get customer edge infos + PageDataEdgeInfo customerEdgeInfos = client.getCustomerEdgeInfos(customerId, 100, 0, + null, TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(1, customerEdgeInfos.getData().size()); + EdgeInfo edgeInfo = customerEdgeInfos.getData().get(0); + assertNotNull(edgeInfo.getCustomerTitle()); + + // unassign edge from customer + Edge unassignedEdge = client.unassignEdgeFromCustomer(edgeId); + assertNotNull(unassignedEdge); + + PageDataEdge customerEdgesAfter = client.getCustomerEdges(customerId, 100, 0, + null, TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(0, customerEdgesAfter.getData().size()); + + // delete edge + UUID edgeToDeleteId = createdEdges.get(0).getId().getId(); + client.deleteEdge(edgeToDeleteId.toString()); + + // verify deletion + assertReturns404(() -> + client.getEdgeById(edgeToDeleteId.toString()) + ); + + PageDataEdge edgesAfterDelete = client.getTenantEdges(100, 0, null, + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(4, edgesAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java new file mode 100644 index 0000000000..5c075f0777 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java @@ -0,0 +1,289 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AliasEntityId; +import org.thingsboard.client.model.Asset; +import org.thingsboard.client.model.AssetTypeFilter; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.DeviceTypeFilter; +import org.thingsboard.client.model.Direction; +import org.thingsboard.client.model.EntityData; +import org.thingsboard.client.model.EntityDataPageLink; +import org.thingsboard.client.model.EntityDataQuery; +import org.thingsboard.client.model.EntityDataSortOrder; +import org.thingsboard.client.model.EntityKey; +import org.thingsboard.client.model.EntityKeyType; +import org.thingsboard.client.model.EntityKeyValueType; +import org.thingsboard.client.model.EntityListFilter; +import org.thingsboard.client.model.EntityNameFilter; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.FilterPredicateValueString; +import org.thingsboard.client.model.KeyFilter; +import org.thingsboard.client.model.PageDataEntityData; +import org.thingsboard.client.model.SingleEntityFilter; +import org.thingsboard.client.model.StringFilterPredicate; +import org.thingsboard.client.model.StringOperation; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class EntityQueryJavaClientTest extends AbstractJavaClientTest { + + private static final String QUERY_TEST_PREFIX = "QueryTest_"; + + private EntityDataPageLink pageLink(int pageSize) { + return new EntityDataPageLink() + .pageSize(pageSize) + .page(0) + .sortOrder(new EntityDataSortOrder() + .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")) + .direction(Direction.ASC)); + } + + @Test + public void testFindByDeviceTypeFilter() throws Exception { + long ts = System.currentTimeMillis(); + String type1 = "temperatureSensor"; + String type2 = "humiditySensor"; + + for (int i = 0; i < 3; i++) { + Device d = new Device(); + d.setName(QUERY_TEST_PREFIX + "temp_" + ts + "_" + i); + d.setType(type1); + client.saveDevice(d, null, null, null, null); + } + for (int i = 0; i < 2; i++) { + Device d = new Device(); + d.setName(QUERY_TEST_PREFIX + "hum_" + ts + "_" + i); + d.setType(type2); + client.saveDevice(d, null, null, null, null); + } + + // filter by single device type + EntityDataQuery singleTypeQuery = new EntityDataQuery() + .entityFilter(new DeviceTypeFilter() + .deviceTypes(List.of(type1))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(singleTypeQuery); + assertNotNull(result); + assertEquals(3, result.getTotalElements().intValue()); + for (EntityData entity : result.getData()) { + assertNotNull(entity.getEntityId()); + } + + // filter by multiple device types + EntityDataQuery multiTypeQuery = new EntityDataQuery() + .entityFilter(new DeviceTypeFilter() + .deviceTypes(List.of(type1, type2))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData multiResult = client.findEntityDataByQuery(multiTypeQuery); + assertNotNull(multiResult); + assertEquals(5, multiResult.getTotalElements().intValue()); + + // filter by device type + name filter + EntityDataQuery nameFilterQuery = new EntityDataQuery() + .entityFilter(new DeviceTypeFilter() + .deviceTypes(List.of(type1, type2)) + .deviceNameFilter(QUERY_TEST_PREFIX + "temp_" + ts)) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData nameResult = client.findEntityDataByQuery(nameFilterQuery); + assertNotNull(nameResult); + assertEquals(3, nameResult.getTotalElements().intValue()); + } + + @Test + public void testFindByEntityNameFilter() throws Exception { + long ts = System.currentTimeMillis(); + String prefix = QUERY_TEST_PREFIX + "named_" + ts; + + for (int i = 0; i < 4; i++) { + Device d = new Device(); + d.setName(prefix + "_" + i); + d.setType("default"); + client.saveDevice(d, null, null, null, null); + } + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityNameFilter() + .entityType(EntityType.DEVICE) + .entityNameFilter(prefix)) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(4, result.getTotalElements().intValue()); + assertFalse(result.getHasNext()); + } + + @Test + public void testFindByEntityListFilter() throws Exception { + long ts = System.currentTimeMillis(); + + Device d1 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_1").type("default"), null, null, null, null); + Device d2 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_2").type("default"), null, null, null, null); + client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_3").type("default"), null, null, null, null); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityListFilter() + .entityType(EntityType.DEVICE) + .entityList(List.of( + d1.getId().getId().toString(), + d2.getId().getId().toString()))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(2, result.getTotalElements().intValue()); + + List returnedIds = result.getData().stream() + .map(e -> e.getEntityId().getId().toString()) + .collect(Collectors.toList()); + assertTrue(returnedIds.contains(d1.getId().getId().toString())); + assertTrue(returnedIds.contains(d2.getId().getId().toString())); + } + + @Test + public void testFindBySingleEntityFilter() throws Exception { + long ts = System.currentTimeMillis(); + Device device = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "single_" + ts).type("default"), null, null, null, null); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new SingleEntityFilter() + .singleEntity(new AliasEntityId() + .id(device.getId().getId()) + .entityType(EntityType.DEVICE))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(1, result.getTotalElements().intValue()); + assertEquals(device.getId().getId().toString(), + result.getData().get(0).getEntityId().getId().toString()); + } + + @Test + public void testFindByAssetTypeFilter() throws Exception { + long ts = System.currentTimeMillis(); + String assetType = "building"; + + for (int i = 0; i < 3; i++) { + Asset a = new Asset(); + a.setName(QUERY_TEST_PREFIX + "asset_" + ts + "_" + i); + a.setType(assetType); + client.saveAsset(a, null, null, null); + } + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new AssetTypeFilter() + .assetTypes(List.of(assetType))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(3, result.getTotalElements().intValue()); + } + + @Test + public void testFindWithKeyFilter() throws Exception { + long ts = System.currentTimeMillis(); + String matchName = QUERY_TEST_PREFIX + "kf_match_" + ts; + String noMatchName = QUERY_TEST_PREFIX + "kf_other_" + ts; + + client.saveDevice(new Device().name(matchName).type("default"), null, null, null, null); + client.saveDevice(new Device().name(noMatchName).type("default"), null, null, null, null); + + KeyFilter nameKeyFilter = new KeyFilter() + .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")) + .valueType(EntityKeyValueType.STRING) + .predicate(new StringFilterPredicate() + .operation(StringOperation.CONTAINS) + .value(new FilterPredicateValueString().defaultValue("kf_match")) + .ignoreCase(true)); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityNameFilter() + .entityType(EntityType.DEVICE) + .entityNameFilter(QUERY_TEST_PREFIX + "kf_")) + .addKeyFiltersItem(nameKeyFilter) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(1, result.getTotalElements().intValue()); + } + + @Test + public void testFindWithPagination() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 5; i++) { + Device d = new Device(); + d.setName(QUERY_TEST_PREFIX + "page_" + ts + "_" + i); + d.setType("default"); + client.saveDevice(d, null, null, null, null); + } + + EntityDataPageLink smallPage = new EntityDataPageLink() + .pageSize(2) + .page(0) + .sortOrder(new EntityDataSortOrder() + .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")) + .direction(Direction.ASC)); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityNameFilter() + .entityType(EntityType.DEVICE) + .entityNameFilter(QUERY_TEST_PREFIX + "page_" + ts)) + .pageLink(smallPage) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + // first page + PageDataEntityData page1 = client.findEntityDataByQuery(query); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + // last page + smallPage.setPage(2); + PageDataEntityData lastPage = client.findEntityDataByQuery(query); + assertNotNull(lastPage); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java new file mode 100644 index 0000000000..5113dc380b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java @@ -0,0 +1,182 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Asset; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntityRelation; +import org.thingsboard.client.model.EntityRelationInfo; +import org.thingsboard.client.model.EntityRelationsQuery; +import org.thingsboard.client.model.EntitySearchDirection; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.RelationEntityTypeFilter; +import org.thingsboard.client.model.RelationTypeGroup; +import org.thingsboard.client.model.RelationsSearchParameters; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class EntityRelationJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testEntityRelationLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create assets and devices to relate + Asset building = new Asset(); + building.setName(TEST_PREFIX + "Building_" + timestamp); + building.setType("building"); + building = client.saveAsset(building, null, null, null); + + Asset floor = new Asset(); + floor.setName(TEST_PREFIX + "Floor_" + timestamp); + floor.setType("floor"); + floor = client.saveAsset(floor, null, null, null); + + Device device1 = new Device(); + device1.setName(TEST_PREFIX + "Sensor_" + timestamp + "_1"); + device1.setType("sensor"); + device1 = client.saveDevice(device1, null, null, null, null); + + Device device2 = new Device(); + device2.setName(TEST_PREFIX + "Sensor_" + timestamp + "_2"); + device2.setType("sensor"); + device2 = client.saveDevice(device2, null, null, null, null); + + Device device3 = new Device(); + device3.setName(TEST_PREFIX + "Sensor_" + timestamp + "_3"); + device3.setType("sensor"); + device3 = client.saveDevice(device3, null, null, null, null); + + // create relations: building -> Contains -> floor, floor -> Contains -> device1/device2/device3 + EntityRelation buildingToFloor = new EntityRelation(); + buildingToFloor.setFrom(building.getId()); + buildingToFloor.setTo(floor.getId()); + buildingToFloor.setType("Contains"); + buildingToFloor.setTypeGroup(RelationTypeGroup.COMMON); + EntityRelation savedRelation = client.saveRelation(buildingToFloor); + assertNotNull(savedRelation); + assertEquals("Contains", savedRelation.getType()); + + client.saveRelation(new EntityRelation() + .from(floor.getId()) + .to(device1.getId()) + .type("Contains") + .typeGroup(RelationTypeGroup.COMMON)); + client.saveRelation(new EntityRelation() + .from(floor.getId()) + .to(device2.getId()) + .type("Contains").typeGroup(RelationTypeGroup.COMMON)); + client.saveRelation(new EntityRelation() + .from(floor.getId()) + .to(device3.getId()) + .type("Manages") + .typeGroup(RelationTypeGroup.COMMON)); + + // get specific relation + EntityRelation fetched = client.getRelation( + building.getId().getId().toString(), "ASSET", + "Contains", + floor.getId().getId().toString(), "ASSET", + RelationTypeGroup.COMMON.getValue()); + assertNotNull(fetched); + assertEquals("Contains", fetched.getType()); + + // find all relations from floor + List fromFloor = client.findEntityRelationsByFrom("ASSET", + floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(3, fromFloor.size()); + + // find relations from floor with type filter "Contains" + List containsFromFloor = client.findEntityRelationsByFromAndRelationType("ASSET", + floor.getId().getId().toString(), "Contains", RelationTypeGroup.COMMON.getValue()); + assertEquals(2, containsFromFloor.size()); + + // find relations to device1 + List toDevice1 = client.findEntityRelationsByTo("DEVICE", + device1.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(1, toDevice1.size()); + assertEquals("Contains", toDevice1.get(0).getType()); + + // find relations to device3 with type filter "Manages" + List managesToDevice3 = client.findEntityRelationsByToAndRelationType("DEVICE", + device3.getId().getId().toString(), "Manages", RelationTypeGroup.COMMON.getValue()); + assertEquals(1, managesToDevice3.size()); + + // find info by from (includes entity names) + List infoFromFloor = client.findEntityRelationInfosByFrom("ASSET", + floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(3, infoFromFloor.size()); + Device finalDevice = device1; + assertTrue(infoFromFloor.stream().anyMatch(info -> + finalDevice.getName().equals(info.getToName()))); + + // find info by to + List infoToDevice2 = client.findEntityRelationInfosByTo("DEVICE", + device2.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(1, infoToDevice2.size()); + assertEquals(floor.getName(), infoToDevice2.get(0).getFromName()); + + // find by query - search from building, direction FROM, max 2 levels + RelationsSearchParameters params = new RelationsSearchParameters(); + params.setRootId(building.getId().getId()); + params.setRootType(EntityType.ASSET); + params.setDirection(EntitySearchDirection.FROM); + params.setRelationTypeGroup(RelationTypeGroup.COMMON); + params.setMaxLevel(2); + + RelationEntityTypeFilter filter = new RelationEntityTypeFilter(); + filter.setRelationType("Contains"); + filter.setEntityTypes(List.of(EntityType.ASSET, EntityType.DEVICE)); + + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(params); + query.setFilters(List.of(filter)); + + List queryResult = client.findEntityRelationsByQuery(query); + assertTrue(queryResult.size() >= 3); + + // find info by query + List infoQueryResult = client.findEntityRelationInfosByQuery(query); + assertTrue(infoQueryResult.size() >= 3); + + // delete single relation + client.deleteRelation( + floor.getId().getId().toString(), "ASSET", + "Manages", + device3.getId().getId().toString(), "DEVICE", + RelationTypeGroup.COMMON.getValue()); + + // verify deletion + List afterDelete = client.findEntityRelationsByFrom("ASSET", + floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(2, afterDelete.size()); + + // delete all relations for building + client.deleteRelations(building.getId().getId().toString(), "ASSET"); + + List afterDeleteAll = client.findEntityRelationsByFrom("ASSET", + building.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(0, afterDeleteAll.size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java new file mode 100644 index 0000000000..453420f784 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java @@ -0,0 +1,267 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AttributesEntityView; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntitySubtype; +import org.thingsboard.client.model.EntityView; +import org.thingsboard.client.model.EntityViewInfo; +import org.thingsboard.client.model.PageDataEntityView; +import org.thingsboard.client.model.PageDataEntityViewInfo; +import org.thingsboard.client.model.TelemetryEntityView; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class EntityViewJavaClientTest extends AbstractJavaClientTest { + + private static final String EV_PREFIX = "EvTest_"; + + @Test + public void testSaveAndGetEntityView() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + + EntityView ev = new EntityView(); + ev.setName(EV_PREFIX + "save_" + ts); + ev.setType("testType"); + ev.setEntityId(device.getId()); + ev.setKeys(new TelemetryEntityView() + .timeseries(List.of("temperature", "humidity")) + .attributes(new AttributesEntityView() + .cs(List.of("firmware")) + .ss(List.of("active")) + .sh(List.of()))); + ev.setStartTimeMs(1000L); + ev.setEndTimeMs(2000L); + + EntityView saved = client.saveEntityView(ev, null, null, null); + assertNotNull(saved); + assertNotNull(saved.getId()); + assertEquals(ev.getName(), saved.getName()); + assertEquals("testType", saved.getType()); + assertEquals(device.getId().getId(), saved.getEntityId().getId()); + assertEquals(List.of("temperature", "humidity"), saved.getKeys().getTimeseries()); + assertEquals(1000L, saved.getStartTimeMs().longValue()); + assertEquals(2000L, saved.getEndTimeMs().longValue()); + + // get by id + String evId = saved.getId().getId().toString(); + EntityView fetched = client.getEntityViewById(evId); + assertNotNull(fetched); + assertEquals(saved.getName(), fetched.getName()); + assertEquals(saved.getType(), fetched.getType()); + assertEquals(saved.getEntityId().getId(), fetched.getEntityId().getId()); + } + + @Test + public void testGetEntityViewInfoById() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "info_" + ts, "infoType", device); + + EntityViewInfo info = client.getEntityViewInfoById(saved.getId().getId().toString()); + assertNotNull(info); + assertEquals(saved.getName(), info.getName()); + assertEquals("infoType", info.getType()); + assertNotNull(info.getEntityId()); + } + + @Test + public void testUpdateEntityView() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "update_" + ts, "default", device); + + saved.setName(EV_PREFIX + "updated_" + ts); + saved.setKeys(new TelemetryEntityView() + .timeseries(List.of("temperature", "pressure")) + .attributes(new AttributesEntityView() + .cs(List.of()) + .ss(List.of()) + .sh(List.of()))); + + EntityView updated = client.saveEntityView(saved, null, null, null); + assertEquals(EV_PREFIX + "updated_" + ts, updated.getName()); + assertEquals(List.of("temperature", "pressure"), updated.getKeys().getTimeseries()); + assertEquals(saved.getId().getId(), updated.getId().getId()); + } + + @Test + public void testDeleteEntityView() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "delete_" + ts, "default", device); + + String evId = saved.getId().getId().toString(); + client.getEntityViewById(evId); + + client.deleteEntityView(evId); + + assertReturns404(() -> client.getEntityViewById(evId)); + } + + @Test + public void testGetTenantEntityViews() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + + for (int i = 0; i < 3; i++) { + createEntityView(EV_PREFIX + "tenant_" + ts + "_" + i, "tenantViewType", device); + } + + PageDataEntityView page = client.getTenantEntityViews(100, 0, null, EV_PREFIX + "tenant_" + ts, null, null); + assertNotNull(page); + assertEquals(3, page.getTotalElements().intValue()); + for (EntityView ev : page.getData()) { + assertTrue(ev.getName().startsWith(EV_PREFIX + "tenant_" + ts)); + } + } + + @Test + public void testGetTenantEntityViewInfos() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + createEntityView(EV_PREFIX + "tinfo_" + ts, "default", device); + + PageDataEntityViewInfo page = client.getTenantEntityViewInfos(100, 0, null, EV_PREFIX + "tinfo_" + ts, null, null); + assertNotNull(page); + assertEquals(1, page.getTotalElements().intValue()); + assertEquals(EV_PREFIX + "tinfo_" + ts, page.getData().get(0).getName()); + } + + @Test + public void testAssignAndUnassignEntityViewToCustomer() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "assign_" + ts, "default", device); + + String evId = saved.getId().getId().toString(); + String customerId = savedClientCustomer.getId().getId().toString(); + + // assign to customer + EntityView assigned = client.assignEntityViewToCustomer(customerId, evId); + assertNotNull(assigned); + assertEquals(savedClientCustomer.getId().getId(), assigned.getCustomerId().getId()); + + // verify in customer entity views + PageDataEntityView customerViews = client.getCustomerEntityViews( + customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null); + assertEquals(1, customerViews.getTotalElements().intValue()); + assertEquals(saved.getName(), customerViews.getData().get(0).getName()); + + // unassign from customer + EntityView unassigned = client.unassignEntityViewFromCustomer(evId); + assertNotNull(unassigned); + + PageDataEntityView afterUnassign = client.getCustomerEntityViews( + customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null); + assertEquals(0, afterUnassign.getTotalElements().intValue()); + } + + @Test + public void testGetCustomerEntityViewInfos() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "cinfo_" + ts, "default", device); + + String evId = saved.getId().getId().toString(); + String customerId = savedClientCustomer.getId().getId().toString(); + + client.assignEntityViewToCustomer(customerId, evId); + + PageDataEntityViewInfo infos = client.getCustomerEntityViewInfos( + customerId, 100, 0, null, EV_PREFIX + "cinfo_" + ts, null, null); + assertNotNull(infos); + assertEquals(1, infos.getTotalElements().intValue()); + assertEquals(saved.getName(), infos.getData().get(0).getName()); + } + + @Test + public void testGetEntityViewTypes() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + createEntityView(EV_PREFIX + "types_" + ts, "uniqueEvType_" + ts, device); + + List types = client.getEntityViewTypes(); + assertNotNull(types); + assertFalse(types.isEmpty()); + + List typeNames = types.stream() + .map(EntitySubtype::getType) + .collect(Collectors.toList()); + assertTrue(typeNames.contains("uniqueEvType_" + ts)); + } + + @Test + public void testGetEntityViewById_notFound() { + String nonExistentId = UUID.randomUUID().toString(); + assertReturns404(() -> client.getEntityViewById(nonExistentId)); + } + + @Test + public void testGetTenantEntityViewsPagination() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + + for (int i = 0; i < 5; i++) { + createEntityView(EV_PREFIX + "paged_" + ts + "_" + i, "default", device); + } + + PageDataEntityView page1 = client.getTenantEntityViews(2, 0, null, EV_PREFIX + "paged_" + ts, null, null); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + PageDataEntityView lastPage = client.getTenantEntityViews(2, 2, null, EV_PREFIX + "paged_" + ts, null, null); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + + private Device createTestDevice(String suffix) throws Exception { + Device device = new Device(); + device.setName(EV_PREFIX + "device_" + suffix); + device.setType("default"); + return client.saveDevice(device, null, null, null, null); + } + + private EntityView createEntityView(String name, String type, Device device) throws Exception { + EntityView ev = new EntityView(); + ev.setName(name); + ev.setType(type); + ev.setEntityId(device.getId()); + ev.setKeys(new TelemetryEntityView() + .timeseries(List.of("temperature")) + .attributes(new AttributesEntityView() + .cs(List.of()) + .ss(List.of()) + .sh(List.of()))); + return client.saveEntityView(ev, null, null, null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java new file mode 100644 index 0000000000..f3f8c2900d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java @@ -0,0 +1,156 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.MobileApp; +import org.thingsboard.client.model.MobileAppBundle; +import org.thingsboard.client.model.MobileAppBundleInfo; +import org.thingsboard.client.model.MobileAppStatus; +import org.thingsboard.client.model.PageDataMobileApp; +import org.thingsboard.client.model.PageDataMobileAppBundleInfo; +import org.thingsboard.client.model.PlatformType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class MobileAppJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testMobileAppLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdApps = new ArrayList<>(); + + // create 3 Android apps + for (int i = 0; i < 3; i++) { + MobileApp app = new MobileApp(); + app.setPkgName("com.test.android." + timestamp + "." + i); + app.setTitle(TEST_PREFIX + "AndroidApp_" + timestamp + "_" + i); + app.setAppSecret("secret_android_" + timestamp + "_" + i); + app.setPlatformType(PlatformType.ANDROID); + app.setStatus(MobileAppStatus.DRAFT); + + MobileApp created = client.saveMobileApp(app); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(app.getPkgName(), created.getPkgName()); + assertEquals(PlatformType.ANDROID, created.getPlatformType()); + assertEquals(MobileAppStatus.DRAFT, created.getStatus()); + + createdApps.add(created); + } + + // create 2 iOS apps + for (int i = 0; i < 2; i++) { + MobileApp app = new MobileApp(); + app.setPkgName("com.test.ios." + timestamp + "." + i); + app.setTitle(TEST_PREFIX + "IosApp_" + timestamp + "_" + i); + app.setAppSecret("secret_ios_" + timestamp + "_" + i); + app.setPlatformType(PlatformType.IOS); + app.setStatus(MobileAppStatus.DRAFT); + + MobileApp created = client.saveMobileApp(app); + assertNotNull(created); + createdApps.add(created); + } + + // list all tenant mobile apps + PageDataMobileApp allApps = client.getTenantMobileApps(100, 0, null, + null, null, null); + assertNotNull(allApps); + assertEquals(5, allApps.getData().size()); + + // list with platform type filter + PageDataMobileApp androidApps = client.getTenantMobileApps(100, 0, PlatformType.ANDROID, + null, null, null); + assertEquals(3, androidApps.getData().size()); + + PageDataMobileApp iosApps = client.getTenantMobileApps(100, 0, PlatformType.IOS, + null, null, null); + assertEquals(2, iosApps.getData().size()); + + // get mobile app by id + MobileApp searchApp = createdApps.get(1); + MobileApp fetchedApp = client.getMobileAppById(searchApp.getId().getId()); + assertEquals(searchApp.getPkgName(), fetchedApp.getPkgName()); + assertEquals(searchApp.getTitle(), fetchedApp.getTitle()); + assertEquals(searchApp.getPlatformType(), fetchedApp.getPlatformType()); + + // update mobile app + MobileApp appToUpdate = createdApps.get(2); + appToUpdate.setTitle(appToUpdate.getTitle() + "_updated"); + MobileApp updatedApp = client.saveMobileApp(appToUpdate); + assertEquals(appToUpdate.getTitle(), updatedApp.getTitle()); + + // create mobile app bundle with android and ios apps + MobileAppBundle bundle = new MobileAppBundle(); + bundle.setTitle(TEST_PREFIX + "Bundle_" + timestamp); + bundle.setDescription("Test bundle"); + bundle.setAndroidAppId(createdApps.get(0).getId()); + bundle.setIosAppId(createdApps.get(3).getId()); + bundle.setOauth2Enabled(false); + + MobileAppBundle savedBundle = client.saveMobileAppBundle(bundle, null); + assertNotNull(savedBundle); + assertNotNull(savedBundle.getId()); + assertEquals(bundle.getTitle(), savedBundle.getTitle()); + + // get bundle info by id + MobileAppBundleInfo bundleInfo = client.getMobileAppBundleInfoById(savedBundle.getId().getId()); + assertEquals(savedBundle.getTitle(), bundleInfo.getTitle()); + assertEquals("Test bundle", bundleInfo.getDescription()); + assertNotNull(bundleInfo.getAndroidPkgName()); + assertNotNull(bundleInfo.getIosPkgName()); + + // list tenant bundles + PageDataMobileAppBundleInfo bundles = client.getTenantMobileAppBundleInfos(100, 0, + TEST_PREFIX + "Bundle_" + timestamp, null, null); + assertEquals(1, bundles.getData().size()); + + // update bundle + savedBundle.setDescription("Updated description"); + MobileAppBundle updatedBundle = client.saveMobileAppBundle(savedBundle, null); + assertEquals("Updated description", updatedBundle.getDescription()); + + // delete bundle + client.deleteMobileAppBundle(savedBundle.getId().getId()); + + // verify bundle deletion + assertReturns404(() -> + client.getMobileAppBundleInfoById(savedBundle.getId().getId()) + ); + + // delete mobile app + UUID appToDeleteId = createdApps.get(0).getId().getId(); + client.deleteMobileApp(appToDeleteId); + + // verify app deletion + assertReturns404(() -> + client.getMobileAppById(appToDeleteId) + ); + + PageDataMobileApp appsAfterDelete = client.getTenantMobileApps(100, 0, null, + null, null, null); + assertEquals(4, appsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java new file mode 100644 index 0000000000..027849d153 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java @@ -0,0 +1,278 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.EntityActionNotificationRuleTriggerConfig; +import org.thingsboard.client.model.EntityActionRecipientsConfig; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.NotificationDeliveryMethod; +import org.thingsboard.client.model.NotificationRequest; +import org.thingsboard.client.model.NotificationRequestInfo; +import org.thingsboard.client.model.NotificationRule; +import org.thingsboard.client.model.NotificationRuleInfo; +import org.thingsboard.client.model.NotificationRuleTriggerType; +import org.thingsboard.client.model.NotificationSettings; +import org.thingsboard.client.model.NotificationTarget; +import org.thingsboard.client.model.NotificationTemplate; +import org.thingsboard.client.model.NotificationTemplateConfig; +import org.thingsboard.client.model.NotificationType; +import org.thingsboard.client.model.PageDataNotification; +import org.thingsboard.client.model.PageDataNotificationRequestInfo; +import org.thingsboard.client.model.PageDataNotificationRuleInfo; +import org.thingsboard.client.model.PageDataNotificationTarget; +import org.thingsboard.client.model.PageDataNotificationTemplate; +import org.thingsboard.client.model.PlatformUsersNotificationTargetConfig; +import org.thingsboard.client.model.TenantAdministratorsFilter; +import org.thingsboard.client.model.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class NotificationJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testNotificationLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // === 1. Notification Target CRUD === + + // Create target + TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter(); + PlatformUsersNotificationTargetConfig targetConfig = + new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter); + NotificationTarget target = + new NotificationTarget() + .name("Test Target " + timestamp) + ._configuration(targetConfig); + + NotificationTarget savedTarget = client.saveNotificationTarget(target); + assertNotNull(savedTarget); + assertNotNull(savedTarget.getId()); + assertEquals("Test Target " + timestamp, savedTarget.getName()); + + // Get target by ID + NotificationTarget fetchedTarget = + client.getNotificationTargetById(savedTarget.getId().getId()); + assertEquals(savedTarget.getName(), fetchedTarget.getName()); + + // List targets + PageDataNotificationTarget targetsPage = + client.getNotificationTargets(100, 0, null, null, null); + assertNotNull(targetsPage); + assertNotNull(targetsPage.getData()); + assertTrue( + targetsPage.getData().stream() + .anyMatch(t -> t.getName().equals(savedTarget.getName()))); + + // Update target + savedTarget.setName("Updated Target " + timestamp); + NotificationTarget updatedTarget = client.saveNotificationTarget(savedTarget); + assertEquals("Updated Target " + timestamp, updatedTarget.getName()); + + // === 2. Notification Template CRUD === + + // Create template + WebDeliveryMethodNotificationTemplate webTemplate = + new WebDeliveryMethodNotificationTemplate() + .subject("Test Subject") + .body("Test notification body") + .enabled(true); + NotificationTemplateConfig templateConfig = + new NotificationTemplateConfig() + .putDeliveryMethodsTemplatesItem("WEB", webTemplate); + NotificationTemplate template = + new NotificationTemplate() + .name("Test Template " + timestamp) + .notificationType(NotificationType.GENERAL) + ._configuration(templateConfig); + + NotificationTemplate savedTemplate = client.saveNotificationTemplate(template); + assertNotNull(savedTemplate); + assertNotNull(savedTemplate.getId()); + assertEquals("Test Template " + timestamp, savedTemplate.getName()); + + // Get template by ID + NotificationTemplate fetchedTemplate = + client.getNotificationTemplateById(savedTemplate.getId().getId()); + assertEquals(savedTemplate.getName(), fetchedTemplate.getName()); + assertEquals(NotificationType.GENERAL, fetchedTemplate.getNotificationType()); + + // List templates + PageDataNotificationTemplate templatesPage = + client.getNotificationTemplates(100, 0, null, null, null, null); + assertNotNull(templatesPage); + assertTrue( + templatesPage.getData().stream() + .anyMatch(t -> t.getName().equals(savedTemplate.getName()))); + + // Update template + savedTemplate.setName("Updated Template " + timestamp); + NotificationTemplate updatedTemplate = client.saveNotificationTemplate(savedTemplate); + assertEquals("Updated Template " + timestamp, updatedTemplate.getName()); + + // === 3. Send notification & read notifications === + + // Send notification request + NotificationRequest request = + new NotificationRequest() + .targets(List.of(savedTarget.getId().getId())) + .templateId(savedTemplate.getId()); + NotificationRequest sentRequest = client.createNotificationRequest(request); + assertNotNull(sentRequest); + assertNotNull(sentRequest.getId()); + + // Get request by ID + NotificationRequestInfo fetchedRequest = + client.getNotificationRequestById(sentRequest.getId().getId()); + assertNotNull(fetchedRequest); + + // List requests + PageDataNotificationRequestInfo requestsPage = + client.getNotificationRequests(100, 0, null, null, null); + assertNotNull(requestsPage); + assertFalse(requestsPage.getData().isEmpty()); + + // Get notifications for current user + PageDataNotification notificationsPage = + client.getNotifications(100, 0, null, null, null, null, null); + assertNotNull(notificationsPage); + assertFalse(notificationsPage.getData().isEmpty()); + + // Get unread count + Integer unreadCount = client.getUnreadNotificationsCount("WEB"); + assertNotNull(unreadCount); + assertTrue("Expected at least one unread notification", unreadCount > 0); + + // Mark single notification as read + client.markNotificationAsRead( + notificationsPage.getData().get(0).getId().getId()); + + // Mark all as read + client.markAllNotificationsAsRead(null); + Integer unreadAfterMarkAll = client.getUnreadNotificationsCount(null); + assertEquals("Expected no unread notifications after marking all as read", 0, unreadAfterMarkAll.intValue()); + + // === 4. Notification Settings === + + NotificationSettings settings = client.getNotificationSettings(); + assertNotNull(settings); + + List deliveryMethods = client.getAvailableDeliveryMethods(); + assertNotNull(deliveryMethods); + assertTrue(deliveryMethods.contains(NotificationDeliveryMethod.WEB)); + + // === 5. Cleanup === + + // Delete notification request + client.deleteNotificationRequest(sentRequest.getId().getId()); + assertReturns404(() -> client.getNotificationRequestById(sentRequest.getId().getId())); + + // Delete template + client.deleteNotificationTemplateById(savedTemplate.getId().getId()); + assertReturns404(() -> client.getNotificationTemplateById(savedTemplate.getId().getId())); + + // Delete target + client.deleteNotificationTargetById(savedTarget.getId().getId()); + assertReturns404(() -> client.getNotificationTargetById(savedTarget.getId().getId())); + } + + @Test + public void testNotificationRuleLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // Create a target for the rule recipients + TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter(); + PlatformUsersNotificationTargetConfig targetConfig = + new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter); + NotificationTarget target = + new NotificationTarget() + .name("Rule Test Target " + timestamp) + ._configuration(targetConfig); + NotificationTarget savedTarget = client.saveNotificationTarget(target); + + // Create a template of type ENTITY_ACTION + WebDeliveryMethodNotificationTemplate webTemplate = + new WebDeliveryMethodNotificationTemplate() + .subject("Entity action: ${entityType}") + .body("Entity ${entityName} was ${actionType}") + .enabled(true); + NotificationTemplateConfig templateConfig = + new NotificationTemplateConfig() + .putDeliveryMethodsTemplatesItem("WEB", webTemplate); + NotificationTemplate template = + new NotificationTemplate() + .name("Rule Test Template " + timestamp) + .notificationType(NotificationType.ENTITY_ACTION) + ._configuration(templateConfig); + NotificationTemplate savedTemplate = client.saveNotificationTemplate(template); + + // Build trigger config: fire on DEVICE create/update + EntityActionNotificationRuleTriggerConfig triggerConfig = + new EntityActionNotificationRuleTriggerConfig() + .addEntityTypesItem(EntityType.DEVICE) + .created(true) + .updated(true) + .deleted(false); + + // Build recipients config + EntityActionRecipientsConfig recipientsConfig = new EntityActionRecipientsConfig() + .addTargetsItem(savedTarget.getId().getId()); + + // saveNotificationRule - create + NotificationRule rule = new NotificationRule() + .name("Test Rule " + timestamp) + .enabled(true) + .templateId(savedTemplate.getId()) + .triggerType(NotificationRuleTriggerType.ENTITY_ACTION) + .triggerConfig(triggerConfig) + .recipientsConfig(recipientsConfig); + + NotificationRule savedRule = client.saveNotificationRule(rule); + assertNotNull(savedRule); + assertNotNull(savedRule.getId()); + assertEquals("Test Rule " + timestamp, savedRule.getName()); + assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, savedRule.getTriggerType()); + assertEquals(Boolean.TRUE, savedRule.getEnabled()); + + // getNotificationRuleById + NotificationRuleInfo fetchedRule = client.getNotificationRuleById(savedRule.getId().getId()); + assertNotNull(fetchedRule); + assertEquals(savedRule.getName(), fetchedRule.getName()); + assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, fetchedRule.getTriggerType()); + + // getNotificationRules - verify it appears in the list + PageDataNotificationRuleInfo rulesPage = client.getNotificationRules(100, 0, null, null, null); + assertNotNull(rulesPage); + assertTrue(rulesPage.getData().stream() + .anyMatch(r -> r.getId().getId().equals(savedRule.getId().getId()))); + + // deleteNotificationRule + client.deleteNotificationRule(savedRule.getId().getId()); + assertReturns404(() -> client.getNotificationRuleById(savedRule.getId().getId())); + + // Cleanup + client.deleteNotificationTemplateById(savedTemplate.getId().getId()); + client.deleteNotificationTargetById(savedTarget.getId().getId()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java new file mode 100644 index 0000000000..835b5fc64e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java @@ -0,0 +1,138 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.MapperType; +import org.thingsboard.client.model.OAuth2BasicMapperConfig; +import org.thingsboard.client.model.OAuth2Client; +import org.thingsboard.client.model.OAuth2ClientInfo; +import org.thingsboard.client.model.OAuth2MapperConfig; +import org.thingsboard.client.model.PageDataOAuth2ClientInfo; +import org.thingsboard.client.model.PlatformType; +import org.thingsboard.client.model.TenantNameStrategyType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class Oauth2JavaClientTest extends AbstractJavaClientTest { + + private OAuth2Client createOAuth2Client(String title, String clientId, String clientSecret) { + OAuth2BasicMapperConfig basicConfig = new OAuth2BasicMapperConfig(); + basicConfig.setEmailAttributeKey("email"); + basicConfig.setFirstNameAttributeKey("given_name"); + basicConfig.setLastNameAttributeKey("family_name"); + basicConfig.setTenantNameStrategy(TenantNameStrategyType.DOMAIN); + + OAuth2MapperConfig mapperConfig = new OAuth2MapperConfig(); + mapperConfig.setType(MapperType.BASIC); + mapperConfig.setAllowUserCreation(true); + mapperConfig.setActivateUser(false); + mapperConfig.setBasic(basicConfig); + + OAuth2Client oAuth2Client = new OAuth2Client(); + oAuth2Client.setTitle(title); + oAuth2Client.setClientId(clientId); + oAuth2Client.setClientSecret(clientSecret); + oAuth2Client.setAuthorizationUri("https://accounts.google.com/o/oauth2/v2/auth"); + oAuth2Client.setAccessTokenUri("https://oauth2.googleapis.com/token"); + oAuth2Client.setScope(List.of("openid", "email", "profile")); + oAuth2Client.setUserInfoUri("https://openidconnect.googleapis.com/v1/userinfo"); + oAuth2Client.setUserNameAttributeName("email"); + oAuth2Client.setClientAuthenticationMethod("POST"); + oAuth2Client.setLoginButtonLabel(title); + oAuth2Client.setMapperConfig(mapperConfig); + oAuth2Client.setPlatforms(List.of(PlatformType.WEB)); + + return oAuth2Client; + } + + @Test + public void testOAuth2ClientLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdClients = new ArrayList<>(); + + // create 5 OAuth2 clients + for (int i = 0; i < 5; i++) { + String title = TEST_PREFIX + "OAuth2_" + timestamp + "_" + i; + OAuth2Client oAuth2Client = createOAuth2Client(title, + "client_id_" + timestamp + "_" + i, + "client_secret_" + timestamp + "_" + i); + + OAuth2Client created = client.saveOAuth2Client(oAuth2Client); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(title, created.getTitle()); + assertEquals("POST", created.getClientAuthenticationMethod()); + assertNotNull(created.getMapperConfig()); + assertEquals(MapperType.BASIC, created.getMapperConfig().getType()); + + createdClients.add(created); + } + + // list tenant OAuth2 client infos + PageDataOAuth2ClientInfo clientInfos = client.findTenantOAuth2ClientInfos(100, 0, + TEST_PREFIX + "OAuth2_" + timestamp, null, null); + assertNotNull(clientInfos); + assertEquals(5, clientInfos.getData().size()); + + // get OAuth2 client by id + OAuth2Client searchClient = createdClients.get(2); + OAuth2Client fetchedClient = client.getOAuth2ClientById(searchClient.getId().getId()); + assertEquals(searchClient.getTitle(), fetchedClient.getTitle()); + assertEquals(searchClient.getClientId(), fetchedClient.getClientId()); + assertEquals(searchClient.getAuthorizationUri(), fetchedClient.getAuthorizationUri()); + assertEquals(3, fetchedClient.getScope().size()); + + // fetch client infos by ids + List idsToFetch = List.of( + createdClients.get(0).getId().getId().toString(), + createdClients.get(1).getId().getId().toString() + ); + List fetchedInfos = client.findTenantOAuth2ClientInfosByIds(idsToFetch); + assertEquals(2, fetchedInfos.size()); + + // update OAuth2 client + OAuth2Client clientToUpdate = client.getOAuth2ClientById(createdClients.get(3).getId().getId()); + clientToUpdate.setTitle(clientToUpdate.getTitle() + "_updated"); + clientToUpdate.setLoginButtonLabel("Updated Login"); + clientToUpdate.setPlatforms(List.of(PlatformType.WEB, PlatformType.ANDROID)); + OAuth2Client updatedClient = client.saveOAuth2Client(clientToUpdate); + assertEquals(clientToUpdate.getTitle(), updatedClient.getTitle()); + assertEquals("Updated Login", updatedClient.getLoginButtonLabel()); + assertEquals(2, updatedClient.getPlatforms().size()); + + // delete OAuth2 client + UUID clientToDeleteId = createdClients.get(0).getId().getId(); + client.deleteOauth2Client(clientToDeleteId); + + // verify deletion + assertReturns404(() -> + client.getOAuth2ClientById(clientToDeleteId) + ); + + PageDataOAuth2ClientInfo clientsAfterDelete = client.findTenantOAuth2ClientInfos(100, 0, + TEST_PREFIX + "OAuth2_" + timestamp, null, null); + assertEquals(4, clientsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java new file mode 100644 index 0000000000..ebd6413b65 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java @@ -0,0 +1,261 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.ChecksumAlgorithm; +import org.thingsboard.client.model.DeviceProfileId; +import org.thingsboard.client.model.DeviceProfileInfo; +import org.thingsboard.client.model.OtaPackage; +import org.thingsboard.client.model.OtaPackageInfo; +import org.thingsboard.client.model.OtaPackageType; +import org.thingsboard.client.model.PageDataOtaPackageInfo; +import org.thingsboard.client.model.SaveOtaPackageInfoRequest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.File; +import java.io.FileWriter; +import java.nio.file.Files; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class OtaPackageJavaClientTest extends AbstractJavaClientTest { + + private static final String OTA_PREFIX = "OtaTest_"; + + private DeviceProfileId getDefaultDeviceProfileId() throws Exception { + DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo(); + return (DeviceProfileId) profileInfo.getId(); + } + + private SaveOtaPackageInfoRequest buildOtaPackageInfoRequest( + String title, String version, OtaPackageType type, + DeviceProfileId deviceProfileId, boolean usesUrl, String url) { + SaveOtaPackageInfoRequest request = new SaveOtaPackageInfoRequest(); + request.setTitle(title); + request.setType(type); + request.setUrl(url); + request.setVersion(version); + request.setDeviceProfileId(deviceProfileId); + return request; + } + + private OtaPackageInfo createFirmwareInfo(String suffix) throws Exception { + DeviceProfileId profileId = getDefaultDeviceProfileId(); + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(), + OtaPackageType.FIRMWARE, profileId, false, null); + return client.saveOtaPackageInfo(request); + } + + private OtaPackageInfo createFirmwareWithUrl(String suffix) throws Exception { + DeviceProfileId profileId = getDefaultDeviceProfileId(); + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(), + OtaPackageType.FIRMWARE, profileId, true, "https://example.com/firmware.bin"); + return client.saveOtaPackageInfo(request); + } + + @Test + public void testSaveAndGetOtaPackageInfo() throws Exception { + long ts = System.currentTimeMillis(); + DeviceProfileId profileId = getDefaultDeviceProfileId(); + String title = OTA_PREFIX + "save_" + ts; + String version = "1.0." + ts; + + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + title, version, OtaPackageType.FIRMWARE, profileId, true, "https://example.com/fw.bin"); + + OtaPackageInfo saved = client.saveOtaPackageInfo(request); + assertNotNull(saved); + assertNotNull(saved.getId()); + assertEquals(title, saved.getTitle()); + assertEquals(version, saved.getVersion()); + assertEquals(OtaPackageType.FIRMWARE, saved.getType()); + assertTrue(saved.getUrl().contains("example.com")); + + // get info by id + String pkgId = saved.getId().getId().toString(); + OtaPackageInfo fetched = client.getOtaPackageInfoById(pkgId); + assertNotNull(fetched); + assertEquals(title, fetched.getTitle()); + assertEquals(version, fetched.getVersion()); + } + + @Test + public void testGetOtaPackageById() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo saved = createFirmwareWithUrl("getbyid_" + ts); + + OtaPackage fullPkg = client.getOtaPackageById(saved.getId().getId().toString()); + assertNotNull(fullPkg); + assertEquals(saved.getTitle(), fullPkg.getTitle()); + assertEquals(saved.getVersion(), fullPkg.getVersion()); + } + + @Test + public void testSaveOtaPackageInfoForSoftware() throws Exception { + long ts = System.currentTimeMillis(); + DeviceProfileId profileId = getDefaultDeviceProfileId(); + String title = OTA_PREFIX + "sw_" + ts; + + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + title, "2.0." + ts, OtaPackageType.SOFTWARE, profileId, true, "https://example.com/sw.bin"); + + OtaPackageInfo saved = client.saveOtaPackageInfo(request); + assertNotNull(saved); + assertEquals(OtaPackageType.SOFTWARE, saved.getType()); + assertEquals(title, saved.getTitle()); + } + + @Test + public void testSaveOtaPackageData() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo info = createFirmwareInfo("data_" + ts); + + File tempFile = Files.createTempFile("ota_test_", ".bin").toFile(); + tempFile.deleteOnExit(); + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write("test firmware content " + ts); + } + + OtaPackageInfo updated = client.saveOtaPackageData( + info.getId().getId().toString(), "MD5", tempFile, null); + assertNotNull(updated); + assertTrue(updated.getHasData()); + assertNotNull(updated.getFileName()); + assertNotNull(updated.getDataSize()); + assertTrue(updated.getDataSize() > 0); + assertEquals(ChecksumAlgorithm.MD5, updated.getChecksumAlgorithm()); + } + + @Test + public void testDownloadOtaPackage() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo info = createFirmwareInfo("download_" + ts); + + String content = "downloadable firmware " + ts; + File tempFile = Files.createTempFile("ota_dl_", ".bin").toFile(); + tempFile.deleteOnExit(); + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write(content); + } + + client.saveOtaPackageData(info.getId().getId().toString(), "MD5", tempFile, null); + + File downloaded = client.downloadOtaPackage(info.getId().getId().toString()); + assertNotNull(downloaded); + assertTrue(downloaded.length() > 0); + String downloadedContent = Files.readString(downloaded.toPath()); + assertEquals(content, downloadedContent); + } + + @Test + public void testDeleteOtaPackage() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo saved = createFirmwareWithUrl("delete_" + ts); + + String pkgId = saved.getId().getId().toString(); + client.getOtaPackageInfoById(pkgId); + + client.deleteOtaPackage(pkgId); + + assertReturns404(() -> client.getOtaPackageInfoById(pkgId)); + } + + @Test + public void testGetOtaPackages() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 3; i++) { + createFirmwareWithUrl("list_" + ts + "_" + i); + } + + PageDataOtaPackageInfo page = client.getOtaPackages(100, 0, OTA_PREFIX + "list_" + ts, null, null); + assertNotNull(page); + assertEquals(3, page.getTotalElements().intValue()); + for (OtaPackageInfo pkg : page.getData()) { + assertTrue(pkg.getTitle().startsWith(OTA_PREFIX + "list_" + ts)); + } + } + + @Test + public void testGetOtaPackagesByDeviceProfileAndType() throws Exception { + long ts = System.currentTimeMillis(); + DeviceProfileId profileId = getDefaultDeviceProfileId(); + + createFirmwareWithUrl("byprofile_" + ts + "_0"); + createFirmwareWithUrl("byprofile_" + ts + "_1"); + + PageDataOtaPackageInfo page = client.getOtaPackagesByDeviceProfileAndType( + profileId.getId().toString(), "FIRMWARE", 100, 0, + OTA_PREFIX + "byprofile_" + ts, null, null); + assertNotNull(page); + assertEquals(2, page.getTotalElements().intValue()); + } + + @Test + public void testGetOtaPackageInfoById_notFound() { + String nonExistentId = UUID.randomUUID().toString(); + assertReturns404(() -> client.getOtaPackageInfoById(nonExistentId)); + } + + @Test + public void testGetOtaPackagesPagination() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 5; i++) { + createFirmwareWithUrl("paged_" + ts + "_" + i); + } + + PageDataOtaPackageInfo page1 = client.getOtaPackages(2, 0, OTA_PREFIX + "paged_" + ts, null, null); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + PageDataOtaPackageInfo lastPage = client.getOtaPackages(2, 2, OTA_PREFIX + "paged_" + ts, null, null); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + + @Test + public void testUpdateOtaPackageInfo() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo saved = createFirmwareWithUrl("update_" + ts); + + SaveOtaPackageInfoRequest updateReq = new SaveOtaPackageInfoRequest(); + updateReq.setId(saved.getId()); + updateReq.setTitle(saved.getTitle()); + updateReq.setType(saved.getType()); + updateReq.setVersion(saved.getVersion()); + updateReq.setDeviceProfileId(saved.getDeviceProfileId()); + updateReq.setUrl(saved.getUrl()); + updateReq.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("infoKey", "infoValue")); + + OtaPackageInfo updated = client.saveOtaPackageInfo(updateReq); + assertNotNull(updated); + assertEquals(saved.getId().getId(), updated.getId().getId()); + assertEquals("infoValue", updated.getAdditionalInfo().get("infoKey").asText()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java new file mode 100644 index 0000000000..399ecd4df9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Device; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.junit.Assert.assertEquals; + +@DaoSqlTest +public class RpcV1JavaClientTest extends AbstractJavaClientTest { + + private static final String ONE_WAY_BODY = + "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}"; + private static final String TWO_WAY_BODY = + "{\"method\":\"getGpio\",\"params\":{\"pin\":7},\"persistent\":true}"; + + @Test + public void testHandleOneWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleOneWayDeviceRPCRequestV1(deviceId, ONE_WAY_BODY); + } catch (ApiException e) { + assertEquals("handleOneWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + @Test + public void testHandleTwoWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleTwoWayDeviceRPCRequestV1(deviceId, TWO_WAY_BODY); + } catch (ApiException e) { + assertEquals("handleTwoWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + private Device createNewDevice(String name) throws ApiException { + Device device = new Device(); + device.setName(name); + device.setType("default"); + return client.saveDevice(device, null, null, null, null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java new file mode 100644 index 0000000000..43669c1620 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.Rpc; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class RpcV2JavaClientTest extends AbstractJavaClientTest { + + private static final String PERSISTENT_BODY = + "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}"; + + @Test + public void testHandleOneWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleOneWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY); + } catch (ApiException e) { + assertEquals("handleOneWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + @Test + public void testHandleTwoWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleTwoWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY); + } catch (ApiException e) { + assertEquals("handleTwoWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + @Test + public void testGetPersistedRpcAndDeleteRpc() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + String rpcId = postPersistentRpcAndGetId(deviceId); + assertNotNull(rpcId); + + Rpc rpc = client.getPersistedRpc(rpcId); + assertNotNull(rpc); + assertNotNull(rpc.getId()); + + client.deleteRpc(rpcId); + + assertReturns404(() -> client.getPersistedRpc(rpcId)); + + client.deleteDevice(deviceId); + } + + @Test + public void testGetPersistedRpcNotFound() { + assertReturns404(() -> client.getPersistedRpc(UUID.randomUUID().toString())); + } + + @Test + public void testGetPersistedRpcByDevice() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + postPersistentRpcAndGetId(deviceId); + + try { + client.getPersistedRpcByDevice(deviceId, 100, 0, null, null, null, null); + } catch (ApiException e) { + assertEquals("getPersistedRpcByDevice got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + private Device createNewDevice(String name) throws ApiException { + Device device = new Device(); + device.setName(name); + device.setType("default"); + return client.saveDevice(device, null, null, null, null); + } + + private String postPersistentRpcAndGetId(String deviceId) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(getBaseUrl() + "/api/plugins/rpc/oneway/" + deviceId)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + client.getToken()) + .POST(HttpRequest.BodyPublishers.ofString(PERSISTENT_BODY)) + .build(); + HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + return OBJECT_MAPPER.readTree(response.body()).get("rpcId").asText(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java new file mode 100644 index 0000000000..95b5301d9f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.NodeConnectionInfo; +import org.thingsboard.client.model.PageDataRuleChain; +import org.thingsboard.client.model.RuleChain; +import org.thingsboard.client.model.RuleChainMetaData; +import org.thingsboard.client.model.RuleChainType; +import org.thingsboard.client.model.RuleNode; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class RuleChainJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testRuleChainAndNodeLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdChains = new ArrayList<>(); + + // create 5 rule chains + for (int i = 0; i < 5; i++) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(TEST_PREFIX + "RuleChain_" + timestamp + "_" + i); + ruleChain.setType(RuleChainType.CORE); + ruleChain.setDebugMode(false); + + RuleChain created = client.saveRuleChain(ruleChain); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(ruleChain.getName(), created.getName()); + assertEquals(RuleChainType.CORE, created.getType()); + + createdChains.add(created); + } + + // list rule chains with text search + PageDataRuleChain filteredChains = client.getRuleChains(100, 0, null, + TEST_PREFIX + "RuleChain_" + timestamp, null, null); + assertNotNull(filteredChains); + assertEquals(5, filteredChains.getData().size()); + + // get rule chain by id + RuleChain searchChain = createdChains.get(2); + RuleChain fetchedChain = client.getRuleChainById(searchChain.getId().getId().toString()); + assertEquals(searchChain.getName(), fetchedChain.getName()); + assertEquals(searchChain.getType(), fetchedChain.getType()); + + // get metadata (initially has default node) + RuleChainMetaData metadata = client.getRuleChainMetaData(searchChain.getId().getId().toString()); + assertNotNull(metadata); + assertEquals(searchChain.getId().getId(), metadata.getRuleChainId().getId()); + + // save metadata with rule nodes and connections + RuleChainMetaData newMetadata = new RuleChainMetaData(metadata.getRuleChainId()); + newMetadata.setVersion(metadata.getVersion()); + newMetadata.setFirstNodeIndex(0); + + // node 0: message type switch + RuleNode switchNode = new RuleNode(); + switchNode.setName("Message Type Switch"); + switchNode.setType("org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode"); + switchNode.setConfiguration(OBJECT_MAPPER.createObjectNode().put("version", 0)); + switchNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 200).put("layoutY", 150)); + + // node 1: log node for telemetry + RuleNode logNode = new RuleNode(); + logNode.setName("Log Telemetry"); + logNode.setType("org.thingsboard.rule.engine.action.TbLogNode"); + logNode.setConfiguration(OBJECT_MAPPER.createObjectNode() + .put("scriptLang", "TBEL") + .put("jsScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);") + .put("tbelScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);")); + logNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 100)); + + // node 2: save timeseries + RuleNode saveNode = new RuleNode(); + saveNode.setName("Save Timeseries"); + saveNode.setType("org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode"); + saveNode.setConfiguration(OBJECT_MAPPER.createObjectNode() + .put("defaultTTL", 0) + .put("skipLatestPersistence", false) + .put("useServerTs", false)); + saveNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 250)); + + newMetadata.setNodes(List.of(switchNode, logNode, saveNode)); + + // connection: switch -> log (on "Post telemetry") + NodeConnectionInfo conn1 = new NodeConnectionInfo(); + conn1.setFromIndex(0); + conn1.setToIndex(1); + conn1.setType("Post telemetry"); + + // connection: switch -> save timeseries (on "Post telemetry") + NodeConnectionInfo conn2 = new NodeConnectionInfo(); + conn2.setFromIndex(0); + conn2.setToIndex(2); + conn2.setType("Post telemetry"); + + newMetadata.setConnections(List.of(conn1, conn2)); + newMetadata.setRuleChainConnections(List.of()); + + RuleChainMetaData savedMetadata = client.saveRuleChainMetaData(newMetadata, false); + assertNotNull(savedMetadata); + assertEquals(3, savedMetadata.getNodes().size()); + assertEquals(2, savedMetadata.getConnections().size()); + + // verify saved nodes + RuleChainMetaData fetchedMetadata = client.getRuleChainMetaData(searchChain.getId().getId().toString()); + assertEquals(3, fetchedMetadata.getNodes().size()); + assertTrue(fetchedMetadata.getNodes().stream() + .anyMatch(node -> "Log Telemetry".equals(node.getName()))); + assertTrue(fetchedMetadata.getNodes().stream() + .anyMatch(node -> "Save Timeseries".equals(node.getName()))); + + // get output labels + client.getRuleChainOutputLabels(searchChain.getId().getId().toString()); + + // update rule chain + RuleChain chainToUpdate = createdChains.get(3); + chainToUpdate.setName(chainToUpdate.getName() + "_updated"); + chainToUpdate.setDebugMode(true); + RuleChain updatedChain = client.saveRuleChain(chainToUpdate); + assertEquals(chainToUpdate.getName(), updatedChain.getName()); + assertEquals(true, updatedChain.getDebugMode()); + + // delete rule chain + UUID chainToDeleteId = createdChains.get(0).getId().getId(); + client.deleteRuleChain(chainToDeleteId.toString()); + + // verify deletion + assertReturns404(() -> + client.getRuleChainById(chainToDeleteId.toString()) + ); + + PageDataRuleChain chainsAfterDelete = client.getRuleChains(100, 0, null, + TEST_PREFIX + "RuleChain_" + timestamp, null, null); + assertEquals(4, chainsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java new file mode 100644 index 0000000000..5dbe358991 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java @@ -0,0 +1,151 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.PageDataTbResourceInfo; +import org.thingsboard.client.model.ResourceExportData; +import org.thingsboard.client.model.TbImageDeleteResult; +import org.thingsboard.client.model.TbResourceInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TbImageJavaClientTest extends AbstractJavaClientTest { + + private File createTempImage(String name, Color color) throws IOException { + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + Graphics2D g = img.createGraphics(); + g.setColor(color); + g.fillRect(0, 0, 100, 100); + g.dispose(); + + File tempFile = File.createTempFile(name, ".png"); + tempFile.deleteOnExit(); + ImageIO.write(img, "png", tempFile); + return tempFile; + } + + @Test + public void testImageLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdImages = new ArrayList<>(); + Color[] colors = {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.CYAN}; + + // upload 5 images + for (int i = 0; i < 5; i++) { + String title = TEST_PREFIX + "Image_" + timestamp + "_" + i; + File imageFile = createTempImage("test_image_" + i, colors[i]); + + TbResourceInfo uploaded = client.uploadImage(imageFile, title, null); + assertNotNull(uploaded); + assertNotNull(uploaded.getResourceKey()); + assertEquals(title, uploaded.getTitle()); + assertNotNull(uploaded.getLink()); + + createdImages.add(uploaded); + } + + // list images with text search + PageDataTbResourceInfo filteredImages = client.getImages(100, 0, null, false, + TEST_PREFIX + "Image_" + timestamp, null, null); + assertNotNull(filteredImages); + assertEquals(5, filteredImages.getData().size()); + + // get image info by type and key + TbResourceInfo searchImage = createdImages.get(2); + TbResourceInfo fetchedInfo = client.getImageInfo("tenant", searchImage.getResourceKey()); + assertEquals(searchImage.getTitle(), fetchedInfo.getTitle()); + assertEquals(searchImage.getResourceKey(), fetchedInfo.getResourceKey()); + + // download image + File downloadedImage = client.downloadImage("tenant", searchImage.getResourceKey(), null, null); + assertNotNull(downloadedImage); + assertTrue(downloadedImage.exists()); + assertTrue(downloadedImage.length() > 0); + + // download image preview + File preview = client.downloadImagePreview("tenant", searchImage.getResourceKey(), null, null); + assertNotNull(preview); + assertTrue(preview.exists()); + assertTrue(preview.length() > 0); + + // update image file + File updatedImageFile = createTempImage("updated_image", Color.MAGENTA); + TbResourceInfo updatedImage = client.updateImage("tenant", searchImage.getResourceKey(), updatedImageFile); + assertNotNull(updatedImage); + assertEquals(searchImage.getResourceKey(), updatedImage.getResourceKey()); + + // update image info (title) + TbResourceInfo infoToUpdate = client.getImageInfo("tenant", createdImages.get(3).getResourceKey()); + infoToUpdate.setTitle(infoToUpdate.getTitle() + "_updated"); + TbResourceInfo updatedInfo = client.updateImageInfo("tenant", infoToUpdate.getResourceKey(), infoToUpdate); + assertEquals(infoToUpdate.getTitle(), updatedInfo.getTitle()); + + // make image public + TbResourceInfo publicImage = client.updateImagePublicStatus("tenant", + createdImages.get(1).getResourceKey(), true); + assertTrue(publicImage.getPublic()); + assertNotNull(publicImage.getPublicResourceKey()); + assertNotNull(publicImage.getPublicLink()); + + // download public image + File publicDownload = client.downloadPublicImage(publicImage.getPublicResourceKey(), null, null); + assertNotNull(publicDownload); + assertTrue(publicDownload.exists()); + assertTrue(publicDownload.length() > 0); + + // make image private again + TbResourceInfo privateImage = client.updateImagePublicStatus("tenant", + createdImages.get(1).getResourceKey(), false); + assertEquals(false, privateImage.getPublic()); + + // export image + ResourceExportData exportData = client.exportImage("tenant", createdImages.get(4).getResourceKey()); + assertNotNull(exportData); + assertNotNull(exportData.getData()); + assertEquals(createdImages.get(4).getTitle(), exportData.getTitle()); + assertEquals(createdImages.get(4).getResourceKey(), exportData.getResourceKey()); + + // delete image + String keyToDelete = createdImages.get(0).getResourceKey(); + TbImageDeleteResult deleteResult = client.deleteImage("tenant", keyToDelete, false); + assertNotNull(deleteResult); + assertTrue(deleteResult.getSuccess()); + + // verify deletion + assertReturns404(() -> + client.getImageInfo("tenant", keyToDelete) + ); + + PageDataTbResourceInfo imagesAfterDelete = client.getImages(100, 0, null, false, + TEST_PREFIX + "Image_" + timestamp, null, null); + assertEquals(4, imagesAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java new file mode 100644 index 0000000000..156667fecd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.PageDataTbResourceInfo; +import org.thingsboard.client.model.ResourceType; +import org.thingsboard.client.model.TbResource; +import org.thingsboard.client.model.TbResourceInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.File; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TbResourceJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testResourceLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdResources = new ArrayList<>(); + + // create 5 JS_MODULE resources + for (int i = 0; i < 5; i++) { + TbResource resource = new TbResource(); + resource.setTitle(TEST_PREFIX + "Resource_" + timestamp + "_" + i); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setResourceKey("test_module_" + timestamp + "_" + i + ".js"); + resource.setFileName("test_module_" + timestamp + "_" + i + ".js"); + + String jsContent = "export default function test" + i + "() { return " + i + "; }"; + resource.setData(Base64.getEncoder().encodeToString(jsContent.getBytes())); + + TbResourceInfo created = client.saveResource(resource); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(resource.getTitle(), created.getTitle()); + assertEquals(ResourceType.JS_MODULE, created.getResourceType()); + + createdResources.add(created); + } + + // get tenant resources, check count + PageDataTbResourceInfo tenantResources = client.getTenantResources(100, 0, null, null, null); + assertNotNull(tenantResources); + assertNotNull(tenantResources.getData()); + int initialSize = tenantResources.getData().size(); + assertTrue("Expected at least 5 resources, but got " + initialSize, initialSize >= 5); + + // find with text search + PageDataTbResourceInfo filteredResources = client.getTenantResources(100, 0, + TEST_PREFIX + "Resource_" + timestamp, null, null); + assertEquals(5, filteredResources.getData().size()); + + // get resources with type filter + PageDataTbResourceInfo jsResources = client.getResources(100, 0, + ResourceType.JS_MODULE.getValue(), null, TEST_PREFIX + "Resource_" + timestamp, null, null); + assertEquals(5, jsResources.getData().size()); + + // get resource info by id + TbResourceInfo searchResource = createdResources.get(2); + TbResourceInfo fetchedInfo = client.getResourceInfoById(searchResource.getId().getId().toString()); + assertEquals(searchResource.getTitle(), fetchedInfo.getTitle()); + assertEquals(searchResource.getResourceKey(), fetchedInfo.getResourceKey()); + + // get full resource by id (includes data) + TbResource fullResource = client.getResourceById(searchResource.getId().getId().toString()); + assertNotNull(fullResource); + assertEquals(searchResource.getTitle(), fullResource.getTitle()); + assertNotNull(fullResource.getData()); + + // download resource + File downloadedFile = client.downloadResource(searchResource.getId().getId().toString()); + assertNotNull(downloadedFile); + assertTrue(downloadedFile.exists()); + assertTrue(downloadedFile.length() > 0); + + // get resources by list of ids + List idsToFetch = List.of( + createdResources.get(0).getId().getId().toString(), + createdResources.get(1).getId().getId().toString() + ); + List resourceList = client.getSystemOrTenantResourcesByIds(idsToFetch); + assertEquals(2, resourceList.size()); + + // update resource + TbResource resourceToUpdate = client.getResourceById(createdResources.get(3).getId().getId().toString()); + resourceToUpdate.setTitle(resourceToUpdate.getTitle() + "_updated"); + String updatedContent = "export default function updated() { return 42; }"; + resourceToUpdate.setData(Base64.getEncoder().encodeToString(updatedContent.getBytes())); + TbResourceInfo updatedResource = client.saveResource(resourceToUpdate); + assertEquals(resourceToUpdate.getTitle(), updatedResource.getTitle()); + + // delete resource + UUID resourceToDeleteId = createdResources.get(0).getId().getId(); + client.deleteResource(resourceToDeleteId.toString(), false); + + // verify deletion + assertReturns404(() -> + client.getResourceInfoById(resourceToDeleteId.toString()) + ); + + PageDataTbResourceInfo resourcesAfterDelete = client.getTenantResources(100, 0, + TEST_PREFIX + "Resource_" + timestamp, null, null); + assertEquals(4, resourcesAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java new file mode 100644 index 0000000000..8028c66682 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java @@ -0,0 +1,150 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AttributeData; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.TsData; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TelemetryJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testTelemetryLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create a device for telemetry operations + Device device = new Device(); + device.setName("TelemetryTestDevice_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + assertNotNull(createdDevice); + + String entityType = "DEVICE"; + String entityId = createdDevice.getId().getId().toString(); + + // save server-side attributes + String serverAttributes = "{\"serverAttr1\": \"value1\", \"serverAttr2\": 42}"; + client.saveEntityAttributesV2(entityType, entityId, "SERVER_SCOPE", serverAttributes); + + // save shared attributes + String sharedAttributes = "{\"sharedAttr1\": \"sharedValue1\", \"sharedAttr2\": true}"; + client.saveEntityAttributesV2(entityType, entityId, "SHARED_SCOPE", sharedAttributes); + + // get attribute keys + List allKeys = client.getAttributeKeys(entityType, entityId); + assertNotNull(allKeys); + assertTrue(allKeys.containsAll(List.of("serverAttr1", "serverAttr2", "sharedAttr1", "sharedAttr2"))); + + // get attribute keys by scope + List serverKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertEquals(2 + 1, serverKeys.size()); //active attribute is automatically added to server scope + assertTrue(serverKeys.containsAll(List.of("serverAttr1", "serverAttr2", "active"))); + + // get attributes by scope + List serverAttrs = client.getAttributesByScope(entityType, entityId, "SERVER_SCOPE", "serverAttr1,serverAttr2", null); + assertNotNull(serverAttrs); + assertEquals(2, serverAttrs.size()); + + // get all attributes + List allAttrs = client.getAttributes(entityType, entityId, "serverAttr1,sharedAttr1", null); + assertEquals(2, allAttrs.size()); + assertEquals("value1", allAttrs.stream().filter(attr -> attr.getKey().equals("serverAttr1")).findFirst().orElseThrow().getValue().toString()); + assertEquals("sharedValue1", allAttrs.stream().filter(attr -> attr.getKey().equals("sharedAttr1")).findFirst().orElseThrow().getValue().toString()); + + // save timeseries data + long ts1 = timestamp - 60000; + long ts2 = timestamp - 30000; + long ts3 = timestamp; + String telemetryBody = "{\"ts\":" + ts1 + ",\"values\":{\"temperature\":25.5,\"humidity\":60}}"; + client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody); + + String telemetryBody2 = "{\"ts\":" + ts2 + ",\"values\":{\"temperature\":26.0,\"humidity\":58}}"; + client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody2); + + String telemetryBody3 = "{\"ts\":" + ts3 + ",\"values\":{\"temperature\":27.1,\"humidity\":55}}"; + client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody3); + + // get timeseries keys + List tsKeys = client.getTimeseriesKeys(entityType, entityId); + assertNotNull(tsKeys); + assertEquals(2, tsKeys.size()); + assertTrue(tsKeys.containsAll(List.of("humidity", "temperature"))); + + // get latest timeseries + Map> latestData = client.getLatestTimeseries(entityType, entityId, "temperature,humidity", false, null); + assertNotNull(latestData); + assertNotNull(latestData.get("temperature")); + assertFalse(latestData.get("temperature").isEmpty()); + assertEquals("27.1", latestData.get("temperature").get(0).getValue().toString()); + + // get timeseries history + Map> historyData = client.getTimeseriesHistory( + entityType, entityId, + ts1 - 1000, ts3 + 1000, "temperature", + null, null, null, null, "NONE", "ASC", false, null); + assertNotNull(historyData); + List tempHistory = historyData.get("temperature"); + assertNotNull(tempHistory); + assertEquals(3, tempHistory.size()); + assertEquals("25.5", tempHistory.get(0).getValue().toString()); + assertEquals("27.1", tempHistory.get(2).getValue().toString()); + + // delete timeseries + client.deleteEntityTimeseries(entityType, entityId, "humidity", true, null, null, true, false, null); + + List keysAfterDelete = client.getTimeseriesKeys(entityType, entityId); + assertFalse(keysAfterDelete.contains("humidity")); + + // delete attributes + client.deleteEntityAttributes(entityType, entityId, "SERVER_SCOPE", "serverAttr1", null); + + List serverKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertFalse(serverKeysAfterDelete.contains("serverAttr1")); + assertTrue(serverKeysAfterDelete.contains("serverAttr2")); + + // save device attributes using device-specific endpoint + client.saveDeviceAttributes(entityId, "SERVER_SCOPE", "{\"deviceSpecificAttr\": \"test\"}"); + + List deviceKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertTrue(deviceKeys.contains("deviceSpecificAttr")); + + // delete device attributes + client.deleteDeviceAttributes(entityId, "SERVER_SCOPE", "deviceSpecificAttr", null); + + List deviceKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertFalse(deviceKeysAfterDelete.contains("deviceSpecificAttr")); + + // save telemetry with TTL + String ttlTelemetry = "{\"ts\":" + timestamp + ",\"values\":{\"shortLived\":99}}"; + client.saveEntityTelemetryWithTTL(entityType, entityId, "ANY", 86400L, ttlTelemetry); + + Map> latestWithTtl = client.getLatestTimeseries(entityType, entityId, "shortLived", false, null); + assertNotNull(latestWithTtl.get("shortLived")); + assertEquals("99", latestWithTtl.get("shortLived").get(0).getValue().toString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java new file mode 100644 index 0000000000..4ab0f2710e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Authority; +import org.thingsboard.client.model.PageDataTenant; +import org.thingsboard.client.model.PageDataUser; +import org.thingsboard.client.model.Tenant; +import org.thingsboard.client.model.User; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class TenantJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testTenantLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdTenants = new ArrayList<>(); + + // authenticate as sysadmin for tenant management + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // create 20 tenants + for (int i = 0; i < 20; i++) { + Tenant tenant = new Tenant(); + String tenantTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + tenant.setTitle(tenantTitle); + tenant.setEmail("tenant_" + timestamp + "_" + i + "@test.com"); + tenant.setCountry("US"); + tenant.setCity("City" + i); + + Tenant createdTenant = client.saveTenant(tenant); + assertNotNull(createdTenant); + assertNotNull(createdTenant.getId()); + assertEquals(tenantTitle, createdTenant.getTitle()); + + createdTenants.add(createdTenant); + } + + try { + // find all with search text, check count + PageDataTenant filteredTenants = client.getTenants(100, 0, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 tenants matching prefix", 10, filteredTenants.getData().size()); + + // find by id + Tenant searchTenant = createdTenants.get(10); + Tenant fetchedTenant = client.getTenantById(searchTenant.getId().getId().toString()); + assertEquals(searchTenant.getTitle(), fetchedTenant.getTitle()); + assertEquals(searchTenant.getEmail(), fetchedTenant.getEmail()); + + // update tenant + fetchedTenant.setCity("Updated City"); + fetchedTenant.setCountry("DE"); + Tenant updatedTenant = client.saveTenant(fetchedTenant); + assertEquals("Updated City", updatedTenant.getCity()); + assertEquals("DE", updatedTenant.getCountry()); + + // create a tenant admin for one of the tenants and verify listing + Tenant tenantForAdmin = createdTenants.get(0); + User adminUser = new User(); + adminUser.setEmail("tenanttest_admin_" + timestamp + "@test.com"); + adminUser.setAuthority(Authority.TENANT_ADMIN); + adminUser.setTenantId(tenantForAdmin.getId()); + adminUser.setFirstName("TestAdmin"); + User savedAdmin = client.saveUser(adminUser, "false"); + assertNotNull(savedAdmin); + + PageDataUser tenantAdmins = client.getTenantAdmins( + tenantForAdmin.getId().getId().toString(), 100, 0, null, null, null); + assertEquals(1, tenantAdmins.getData().size()); + assertEquals(savedAdmin.getEmail(), tenantAdmins.getData().get(0).getEmail()); + + // delete tenant + UUID tenantToDeleteId = createdTenants.get(0).getId().getId(); + client.deleteTenant(tenantToDeleteId.toString()); + createdTenants.remove(0); + + // verify deletion + PageDataTenant tenantsAfterDelete = client.getTenants(100, 0, TEST_PREFIX_2, null, null); + assertEquals(10, tenantsAfterDelete.getData().size()); + + assertReturns404(() -> + client.getTenantById(tenantToDeleteId.toString()) + ); + } finally { + // clean up all created tenants (deleting tenant cascades to users) + client.login("sysadmin@thingsboard.org", "sysadmin"); + for (Tenant tenant : createdTenants) { + try { + client.deleteTenant(tenant.getId().getId().toString()); + } catch (ApiException ignored) { + } + } + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java new file mode 100644 index 0000000000..e8c110a004 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java @@ -0,0 +1,179 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.DefaultTenantProfileConfiguration; +import org.thingsboard.client.model.EntityInfo; +import org.thingsboard.client.model.PageDataEntityInfo; +import org.thingsboard.client.model.PageDataTenantProfile; +import org.thingsboard.client.model.TenantProfile; +import org.thingsboard.client.model.TenantProfileData; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TenantProfileJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testTenantProfileLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdProfiles = new ArrayList<>(); + + // authenticate as sysadmin for tenant profile management + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // get initial count (there should be a default profile) + PageDataTenantProfile initialProfiles = client.getTenantProfiles(100, 0, null, null, null); + assertNotNull(initialProfiles); + int initialSize = initialProfiles.getData().size(); + assertTrue("Expected at least 1 default tenant profile", initialSize >= 1); + + // get default tenant profile info + EntityInfo defaultProfileInfo = client.getDefaultTenantProfileInfo(); + assertNotNull(defaultProfileInfo); + assertNotNull(defaultProfileInfo.getName()); + + try { + // create 5 tenant profiles + for (int i = 0; i < 5; i++) { + TenantProfile profile = new TenantProfile(); + profile.setName(TEST_PREFIX + "TenantProfile_" + timestamp + "_" + i); + profile.setDescription("Test tenant profile " + i); + profile.setIsolatedTbRuleEngine(false); + + TenantProfileData profileData = new TenantProfileData(); + DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration(); + config.setMaxDevices(100L); + config.setMaxAssets(100L); + config.setMaxCustomers(50L); + config.setMaxUsers(50L); + config.setMaxDashboards(50L); + config.setMaxRuleChains(20L); + config.setMaxDataPointsPerRollingArg(20L); + config.setMaxRelatedEntitiesToReturnPerCfArgument(20); + config.setMaxRelationLevelPerCfArgument(20); + profileData.setConfiguration(config); + profile.setProfileData(profileData); + profile.setDefault(false); + + TenantProfile created = client.saveTenantProfile(profile); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(profile.getName(), created.getName()); + assertEquals(profile.getDescription(), created.getDescription()); + assertFalse(created.getDefault()); + + createdProfiles.add(created); + } + + // find all, check count + PageDataTenantProfile allProfiles = client.getTenantProfiles(100, 0, null, null, null); + assertNotNull(allProfiles); + assertEquals(initialSize + 5, allProfiles.getData().size()); + + // find with text search + PageDataTenantProfile filteredProfiles = client.getTenantProfiles(100, 0, + TEST_PREFIX + "TenantProfile_" + timestamp, null, null); + assertEquals(5, filteredProfiles.getData().size()); + + // get by id + TenantProfile searchProfile = createdProfiles.get(2); + TenantProfile fetchedProfile = client.getTenantProfileById(searchProfile.getId().getId().toString()); + assertEquals(searchProfile.getName(), fetchedProfile.getName()); + assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription()); + + // update tenant profile + fetchedProfile.setDescription("Updated description"); + TenantProfile updatedProfile = client.saveTenantProfile(fetchedProfile); + assertEquals("Updated description", updatedProfile.getDescription()); + assertEquals(fetchedProfile.getName(), updatedProfile.getName()); + + // get tenant profile infos (paginated) + PageDataEntityInfo profileInfos = client.getTenantProfileInfos(100, 0, null, null, null); + assertNotNull(profileInfos); + assertEquals(initialSize + 5, profileInfos.getData().size()); + + // get profiles by list of ids + List idsToFetch = List.of( + createdProfiles.get(0).getId().getId().toString(), + createdProfiles.get(1).getId().getId().toString() + ); + List profileList = client.getTenantProfileList(idsToFetch); + assertEquals(2, profileList.size()); + + // set a profile as default + TenantProfile profileToSetDefault = createdProfiles.get(1); + client.setDefaultTenantProfile(profileToSetDefault.getId().getId().toString()); + EntityInfo defaultTenantProfileInfo = client.getDefaultTenantProfileInfo(); + assertEquals(profileToSetDefault.getName(), defaultTenantProfileInfo.getName()); + + // verify default profile info now points to the new default + EntityInfo newDefaultInfo = client.getDefaultTenantProfileInfo(); + assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName()); + + // restore original default profile + TenantProfile originalDefault = initialProfiles.getData().stream() + .filter(TenantProfile::getDefault) + .findFirst() + .orElseThrow(); + client.setDefaultTenantProfile(originalDefault.getId().getId().toString()); + + // delete tenant profile (cannot delete the default one) + UUID profileToDeleteId = createdProfiles.get(0).getId().getId(); + client.deleteTenantProfile(profileToDeleteId.toString()); + createdProfiles.remove(0); + + // verify deletion + assertReturns404(() -> + client.getTenantProfileById(profileToDeleteId.toString()) + ); + + PageDataTenantProfile profilesAfterDelete = client.getTenantProfiles(100, 0, null, null, null); + assertEquals(initialSize + 4, profilesAfterDelete.getData().size()); + } finally { + // clean up created profiles + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // ensure original default is restored before deleting test profiles + TenantProfile originalDefault = initialProfiles.getData().stream() + .filter(TenantProfile::getDefault) + .findFirst() + .orElseThrow(); + try { + client.setDefaultTenantProfile(originalDefault.getId().getId().toString()); + } catch (ApiException ignored) { + } + + for (TenantProfile profile : createdProfiles) { + try { + client.deleteTenantProfile(profile.getId().getId().toString()); + } catch (ApiException ignored) { + } + } + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java new file mode 100644 index 0000000000..972049d44c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.AccountTwoFaSettings; +import org.thingsboard.client.model.PlatformTwoFaSettings; +import org.thingsboard.client.model.TotpTwoFaAccountConfig; +import org.thingsboard.client.model.TotpTwoFaProviderConfig; +import org.thingsboard.client.model.TwoFaAccountConfig; +import org.thingsboard.client.model.TwoFaProviderType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TwoFactorAuthJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testTwoFactorAuthLifecycle() throws Exception { + // save original platform 2FA settings as sysadmin + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // configure platform 2FA settings with TOTP provider + TotpTwoFaProviderConfig totpProviderConfig = new TotpTwoFaProviderConfig(); + totpProviderConfig.setIssuerName("TestThingsBoard"); + + PlatformTwoFaSettings newSettings = new PlatformTwoFaSettings(); + newSettings.setProviders(List.of(totpProviderConfig)); + newSettings.setMinVerificationCodeSendPeriod(30); + newSettings.setTotalAllowedTimeForVerification(300); + newSettings.setMaxVerificationFailuresBeforeUserLockout(5); + + PlatformTwoFaSettings savedSettings = client.savePlatformTwoFaSettings(newSettings); + assertNotNull(savedSettings); + assertNotNull(savedSettings.getProviders()); + assertFalse(savedSettings.getProviders().isEmpty()); + assertEquals(30, savedSettings.getMinVerificationCodeSendPeriod().intValue()); + assertEquals(300, savedSettings.getTotalAllowedTimeForVerification().intValue()); + + // get available 2FA providers (should include TOTP) + List providerTypes = client.getAvailableTwoFaProviderTypes(); + assertNotNull(providerTypes); + assertTrue(providerTypes.contains(TwoFaProviderType.TOTP)); + + // get account 2FA settings (should be empty initially) + AccountTwoFaSettings accountSettings = client.getAccountTwoFaSettings(); + assertNull(accountSettings); + + // generate TOTP account config + TwoFaAccountConfig generatedConfig = client.generateTwoFaAccountConfig(TwoFaProviderType.TOTP.getValue()); + assertNotNull(generatedConfig); + TotpTwoFaAccountConfig totpConfig = (TotpTwoFaAccountConfig) generatedConfig; + assertNotNull(totpConfig); + assertNotNull(totpConfig.getAuthUrl()); + assertTrue(totpConfig.getAuthUrl().startsWith("otpauth://totp/")); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java new file mode 100644 index 0000000000..378f48846f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.Authority; +import org.thingsboard.client.model.Customer; +import org.thingsboard.client.model.JwtPair; +import org.thingsboard.client.model.PageDataUser; +import org.thingsboard.client.model.User; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class UserJavaClientTest extends AbstractJavaClientTest { + + @Test + public void testUserLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdUsers = new ArrayList<>(); + + // create 20 tenant admin users + for (int i = 0; i < 20; i++) { + User user = new User(); + String email = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i + "@test.com"; + user.setEmail(email); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(savedClientTenant.getId()); + user.setFirstName("First" + i); + user.setLastName("Last" + i); + + User createdUser = client.saveUser(user, "false"); + assertNotNull(createdUser); + assertNotNull(createdUser.getId()); + assertEquals(email, createdUser.getEmail()); + assertEquals(Authority.TENANT_ADMIN, createdUser.getAuthority()); + + createdUsers.add(createdUser); + } + + // find all tenant admins, check count (20 created + 1 from setup) + PageDataUser allUsers = client.getUsers(100, 0, null, null, null); + assertNotNull(allUsers); + assertNotNull(allUsers.getData()); + int initialSize = allUsers.getData().size(); + assertEquals("Expected 21 users (20 created + 2 from setup), but got " + initialSize, 22, initialSize); + + // find with search text, check count + PageDataUser filteredUsers = client.getUsers(100, 0, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 users matching prefix", 10, filteredUsers.getData().size()); + + // find by id + User searchUser = createdUsers.get(10); + User fetchedUser = client.getUserById(searchUser.getId().getId().toString()); + assertEquals(searchUser.getEmail(), fetchedUser.getEmail()); + assertEquals(searchUser.getFirstName(), fetchedUser.getFirstName()); + + // update user + fetchedUser.setFirstName("UpdatedFirst"); + fetchedUser.setLastName("UpdatedLast"); + User updatedUser = client.saveUser(fetchedUser, "false"); + assertEquals("UpdatedFirst", updatedUser.getFirstName()); + assertEquals("UpdatedLast", updatedUser.getLastName()); + + // activate user and get token + activateUser(createdUsers.get(0).getId(), "password123", false); + JwtPair userToken = client.getUserToken(createdUsers.get(0).getId().getId().toString()); + assertNotNull(userToken); + assertNotNull(userToken.getToken()); + + // disable user credentials + client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "false"); + + // re-enable user credentials + client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "true"); + + // create customer users and verify listing + Customer customer2 = new Customer(); + customer2.setTitle("User test customer " + timestamp); + customer2.setEmail("usertest_" + timestamp + "@test.com"); + Customer savedCustomer2 = client.saveCustomer(customer2, null, null, null); + + List customerUsers = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + User customerUser = new User(); + customerUser.setEmail("custuser_" + timestamp + "_" + i + "@test.com"); + customerUser.setAuthority(Authority.CUSTOMER_USER); + customerUser.setTenantId(savedClientTenant.getId()); + customerUser.setCustomerId(savedCustomer2.getId()); + customerUser.setFirstName("CustFirst" + i); + customerUser.setLastName("CustLast" + i); + + User created = client.saveUser(customerUser, "false"); + assertNotNull(created); + customerUsers.add(created); + } + + // list customer users + PageDataUser customerUserPage = client.getCustomerUsers( + savedCustomer2.getId().getId().toString(), 100, 0, null, null, null); + assertEquals("Expected 5 customer users", 5, customerUserPage.getData().size()); + + // delete user + UUID userToDeleteId = createdUsers.get(0).getId().getId(); + client.deleteUser(userToDeleteId.toString()); + + // verify deletion + PageDataUser usersAfterDelete = client.getUsers(100, 0, null, null, null); + assertEquals(initialSize + 5 - 1, usersAfterDelete.getData().size()); + + assertReturns404(() -> + client.getUserById(userToDeleteId.toString()) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java new file mode 100644 index 0000000000..503c8efcf5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java @@ -0,0 +1,155 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; +import org.thingsboard.client.model.PageDataWidgetTypeInfo; +import org.thingsboard.client.model.WidgetTypeDetails; +import org.thingsboard.client.model.WidgetTypeInfo; +import org.thingsboard.client.model.WidgetsBundle; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class WidgetTypeJavaClientTest extends AbstractJavaClientTest { + + private JsonNode createDescriptor(String type) { + return OBJECT_MAPPER.createObjectNode() + .put("type", type) + .put("sizeX", 7.5) + .put("sizeY", 5) + .put("resources", "[]") + .put("templateHtml", "
Test
") + .put("templateCss", ".test-widget { font-size: 14px; }") + .put("controllerScript", "self.onInit = function() {};") + .put("settingsSchema", "{}") + .put("dataKeySettingsSchema", "{}"); + } + + @Test + public void testWidgetTypeLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdWidgetTypes = new ArrayList<>(); + + // create a widgets bundle + WidgetsBundle bundle = new WidgetsBundle(null, null, null, + TEST_PREFIX + "Bundle_" + timestamp, null, false, + "Test bundle description", null, null); + WidgetsBundle savedBundle = client.saveWidgetsBundle(bundle); + assertNotNull(savedBundle); + assertNotNull(savedBundle.getId()); + assertEquals(bundle.getTitle(), savedBundle.getTitle()); + + // create 5 widget types + for (int i = 0; i < 5; i++) { + String name = TEST_PREFIX + "Widget_" + timestamp + "_" + i; + JsonNode descriptor = createDescriptor("latest"); + + WidgetTypeDetails widgetType = new WidgetTypeDetails(null, null, null, name, descriptor); + widgetType.setDescription("Test widget " + i); + widgetType.setDeprecated(false); + widgetType.setTags(List.of("test", "automated")); + + WidgetTypeDetails created = client.saveWidgetType(widgetType, false); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(name, created.getName()); + assertNotNull(created.getFqn()); + + createdWidgetTypes.add(created); + } + + // list widget types with text search (tenant only) + PageDataWidgetTypeInfo filteredTypes = client.getWidgetTypes(100, 0, + TEST_PREFIX + "Widget_" + timestamp, null, null, + true, false, null, null, null); + assertNotNull(filteredTypes); + assertEquals(5, filteredTypes.getData().size()); + + // get widget type details by id + WidgetTypeDetails searchWidget = createdWidgetTypes.get(2); + WidgetTypeDetails fetchedDetails = client.getWidgetTypeById( + searchWidget.getId().getId().toString(), true); + assertEquals(searchWidget.getName(), fetchedDetails.getName()); + assertEquals(searchWidget.getFqn(), fetchedDetails.getFqn()); + assertEquals("Test widget 2", fetchedDetails.getDescription()); + + // get widget type info by id + WidgetTypeInfo fetchedInfo = client.getWidgetTypeInfoById( + searchWidget.getId().getId().toString()); + assertEquals(searchWidget.getName(), fetchedInfo.getName()); + + // add widget types to bundle + List widgetTypeIds = createdWidgetTypes.stream() + .map(wt -> wt.getId().getId().toString()) + .collect(Collectors.toList()); + client.updateWidgetsBundleWidgetTypes(savedBundle.getId().getId().toString(), widgetTypeIds); + + // get bundle widget type fqns + List bundleFqns = client.getBundleWidgetTypeFqns(savedBundle.getId().getId().toString()); + assertEquals(5, bundleFqns.size()); + + // get bundle widget types details + List bundleDetails = client.getBundleWidgetTypesDetails( + savedBundle.getId().getId().toString(), false); + assertEquals(5, bundleDetails.size()); + + // get bundle widget types infos (paginated) + PageDataWidgetTypeInfo bundleInfos = client.getBundleWidgetTypesInfos( + savedBundle.getId().getId().toString(), 100, 0, + null, null, null, null, null, null); + assertEquals(5, bundleInfos.getData().size()); + + // update widget type + WidgetTypeDetails widgetToUpdate = client.getWidgetTypeById( + createdWidgetTypes.get(3).getId().getId().toString(), true); + widgetToUpdate.setDescription("Updated description"); + widgetToUpdate.setDeprecated(true); + widgetToUpdate.setTags(List.of("test", "updated")); + WidgetTypeDetails updatedWidget = client.saveWidgetType(widgetToUpdate, false); + assertEquals("Updated description", updatedWidget.getDescription()); + assertEquals(true, updatedWidget.getDeprecated()); + + // delete widget type + String widgetToDeleteId = createdWidgetTypes.get(0).getId().getId().toString(); + client.deleteWidgetType(widgetToDeleteId); + + // verify deletion + assertReturns404(() -> + client.getWidgetTypeById(widgetToDeleteId, false) + ); + + PageDataWidgetTypeInfo typesAfterDelete = client.getWidgetTypes(100, 0, + TEST_PREFIX + "Widget_" + timestamp, null, null, + true, false, null, null, null); + assertEquals(4, typesAfterDelete.getData().size()); + + // delete widgets bundle + client.deleteWidgetsBundle(savedBundle.getId().getId().toString()); + + assertReturns404(() -> + client.getWidgetsBundleById(savedBundle.getId().getId().toString(), false) + ); + } + +} diff --git a/pom.xml b/pom.xml index 4dcc3929fd..6cea12b1bd 100755 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ ${project.name} /var/log/${pkg.name} /usr/share/${pkg.name} + 4.4.0-SNAPSHOT 3.4.13 10.1.52 2.18.6 @@ -1134,6 +1135,12 @@ ${project.version} test
+ + org.thingsboard.client + thingsboard-ce-client + ${thingsboard.client.version} + test + org.thingsboard.msa js-executor From a08571ea2719b5a2fda974f0c0403f877c2484ea Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 18 Mar 2026 15:02:23 +0200 Subject: [PATCH 168/313] Fix AdminJavaClientTest --- .../org/thingsboard/server/client/AdminJavaClientTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java index 4c6b3444b0..8cfba21af2 100644 --- a/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java @@ -89,12 +89,11 @@ public class AdminJavaClientTest extends AbstractJavaClientTest { FeaturesInfo featuresInfo = client.getFeaturesInfo(); assertNotNull(featuresInfo); assertFalse(featuresInfo.getSmsEnabled()); - assertTrue(featuresInfo.getOauthEnabled()); + assertFalse(featuresInfo.getOauthEnabled()); // check updates UpdateMessage updateMessage = client.checkUpdates(); assertNotNull(updateMessage); - assertNotNull(updateMessage.getCurrentVersion()); } } From b26e018d241ef303f519074176960b9e859592d8 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 18 Mar 2026 15:18:45 +0200 Subject: [PATCH 169/313] Rename JavaClientTest classes to ApiClientTest --- ...AIModelJavaClientTest.java => AIModelApiClientTest.java} | 2 +- ...stractJavaClientTest.java => AbstractApiClientTest.java} | 6 +++--- .../{AdminJavaClientTest.java => AdminApiClientTest.java} | 2 +- .../{AlarmJavaClientTest.java => AlarmApiClientTest.java} | 2 +- ...ntJavaClientTest.java => AlarmCommentApiClientTest.java} | 2 +- .../{AssetJavaClientTest.java => AssetApiClientTest.java} | 2 +- ...leJavaClientTest.java => AssetProfileApiClientTest.java} | 2 +- ...avaClientTest.java => CalculatedFieldApiClientTest.java} | 2 +- ...stomerJavaClientTest.java => CustomerApiClientTest.java} | 4 ++-- ...boardJavaClientTest.java => DashboardApiClientTest.java} | 2 +- .../{DeviceJavaClientTest.java => DeviceApiClientTest.java} | 2 +- ...ClientTest.java => DeviceConnectivityApiClientTest.java} | 2 +- ...eJavaClientTest.java => DeviceProfileApiClientTest.java} | 2 +- .../{DomainJavaClientTest.java => DomainApiClientTest.java} | 2 +- .../{EdgeJavaClientTest.java => EdgeApiClientTest.java} | 2 +- ...eryJavaClientTest.java => EntityQueryApiClientTest.java} | 2 +- ...JavaClientTest.java => EntityRelationApiClientTest.java} | 2 +- ...ViewJavaClientTest.java => EntityViewApiClientTest.java} | 2 +- ...leAppJavaClientTest.java => MobileAppApiClientTest.java} | 2 +- ...onJavaClientTest.java => NotificationApiClientTest.java} | 2 +- .../{Oauth2JavaClientTest.java => Oauth2ApiClientTest.java} | 2 +- ...kageJavaClientTest.java => OtaPackageApiClientTest.java} | 2 +- .../{RpcV1JavaClientTest.java => RpcV1ApiClientTest.java} | 2 +- .../{RpcV2JavaClientTest.java => RpcV2ApiClientTest.java} | 2 +- ...ChainJavaClientTest.java => RuleChainApiClientTest.java} | 2 +- ...TbImageJavaClientTest.java => TbImageApiClientTest.java} | 2 +- ...urceJavaClientTest.java => TbResourceApiClientTest.java} | 2 +- ...metryJavaClientTest.java => TelemetryApiClientTest.java} | 2 +- .../{TenantJavaClientTest.java => TenantApiClientTest.java} | 2 +- ...eJavaClientTest.java => TenantProfileApiClientTest.java} | 2 +- ...hJavaClientTest.java => TwoFactorAuthApiClientTest.java} | 2 +- .../{UserJavaClientTest.java => UserApiClientTest.java} | 2 +- ...TypeJavaClientTest.java => WidgetTypeApiClientTest.java} | 2 +- 33 files changed, 36 insertions(+), 36 deletions(-) rename application/src/test/java/org/thingsboard/server/client/{AIModelJavaClientTest.java => AIModelApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{AbstractJavaClientTest.java => AbstractApiClientTest.java} (95%) rename application/src/test/java/org/thingsboard/server/client/{AdminJavaClientTest.java => AdminApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{AlarmJavaClientTest.java => AlarmApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{AlarmCommentJavaClientTest.java => AlarmCommentApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{AssetJavaClientTest.java => AssetApiClientTest.java} (97%) rename application/src/test/java/org/thingsboard/server/client/{AssetProfileJavaClientTest.java => AssetProfileApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{CalculatedFieldJavaClientTest.java => CalculatedFieldApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{CustomerJavaClientTest.java => CustomerApiClientTest.java} (97%) rename application/src/test/java/org/thingsboard/server/client/{DashboardJavaClientTest.java => DashboardApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{DeviceJavaClientTest.java => DeviceApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{DeviceConnectivityJavaClientTest.java => DeviceConnectivityApiClientTest.java} (96%) rename application/src/test/java/org/thingsboard/server/client/{DeviceProfileJavaClientTest.java => DeviceProfileApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{DomainJavaClientTest.java => DomainApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{EdgeJavaClientTest.java => EdgeApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{EntityQueryJavaClientTest.java => EntityQueryApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{EntityRelationJavaClientTest.java => EntityRelationApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{EntityViewJavaClientTest.java => EntityViewApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{MobileAppJavaClientTest.java => MobileAppApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{NotificationJavaClientTest.java => NotificationApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{Oauth2JavaClientTest.java => Oauth2ApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{OtaPackageJavaClientTest.java => OtaPackageApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{RpcV1JavaClientTest.java => RpcV1ApiClientTest.java} (97%) rename application/src/test/java/org/thingsboard/server/client/{RpcV2JavaClientTest.java => RpcV2ApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{RuleChainJavaClientTest.java => RuleChainApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{TbImageJavaClientTest.java => TbImageApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{TbResourceJavaClientTest.java => TbResourceApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{TelemetryJavaClientTest.java => TelemetryApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{TenantJavaClientTest.java => TenantApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{TenantProfileJavaClientTest.java => TenantProfileApiClientTest.java} (99%) rename application/src/test/java/org/thingsboard/server/client/{TwoFactorAuthJavaClientTest.java => TwoFactorAuthApiClientTest.java} (97%) rename application/src/test/java/org/thingsboard/server/client/{UserJavaClientTest.java => UserApiClientTest.java} (98%) rename application/src/test/java/org/thingsboard/server/client/{WidgetTypeJavaClientTest.java => WidgetTypeApiClientTest.java} (98%) diff --git a/application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java index 5a0963aa3f..59314378aa 100644 --- a/application/src/test/java/org/thingsboard/server/client/AIModelJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java @@ -30,7 +30,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class AIModelJavaClientTest extends AbstractJavaClientTest { +public class AIModelApiClientTest extends AbstractApiClientTest { private static final String AI_PREFIX = "AiTest_"; diff --git a/application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java similarity index 95% rename from application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java index 8323915184..6bbda65a59 100644 --- a/application/src/test/java/org/thingsboard/server/client/AbstractJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java @@ -33,13 +33,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @Slf4j -public abstract class AbstractJavaClientTest extends AbstractControllerTest { +public abstract class AbstractApiClientTest extends AbstractControllerTest { protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); protected static final ObjectMapper MAPPER = new ObjectMapper(); - protected static final String TEST_PREFIX = "JavaClientTestDevice_"; - protected static final String TEST_PREFIX_2 = "JavaClientTestDevice2_"; + protected static final String TEST_PREFIX = "ApiClientTestDevice_"; + protected static final String TEST_PREFIX_2 = "ApiClientTestDevice2_"; protected static final String CUSTOMER_USERNAME = "javaClientCustomer@thingsboard.org"; protected static final String TENANT_ADMIN_USERNAME = "javaClientTenant@thingsboard.org"; protected static final String TEST_PASSWORD = "password123"; diff --git a/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java index 8cfba21af2..4d11bf3fbe 100644 --- a/application/src/test/java/org/thingsboard/server/client/AdminJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java @@ -31,7 +31,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class AdminJavaClientTest extends AbstractJavaClientTest { +public class AdminApiClientTest extends AbstractApiClientTest { @Test public void testAdminSettingsLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java index 61666297dc..0ebc247018 100644 --- a/application/src/test/java/org/thingsboard/server/client/AlarmJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java @@ -38,7 +38,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class AlarmJavaClientTest extends AbstractJavaClientTest { +public class AlarmApiClientTest extends AbstractApiClientTest { @Test public void testAlarmLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java index 6b519d5fd0..31da96b7b4 100644 --- a/application/src/test/java/org/thingsboard/server/client/AlarmCommentJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java @@ -34,7 +34,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class AlarmCommentJavaClientTest extends AbstractJavaClientTest { +public class AlarmCommentApiClientTest extends AbstractApiClientTest { @Test public void testAlarmComments() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java similarity index 97% rename from application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java index 27cde622a4..070470ac2a 100644 --- a/application/src/test/java/org/thingsboard/server/client/AssetJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java @@ -28,7 +28,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class AssetJavaClientTest extends AbstractJavaClientTest { +public class AssetApiClientTest extends AbstractApiClientTest { @Test public void testAssetLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java index 7825a047bd..7f6a2f667b 100644 --- a/application/src/test/java/org/thingsboard/server/client/AssetProfileJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java @@ -32,7 +32,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class AssetProfileJavaClientTest extends AbstractJavaClientTest { +public class AssetProfileApiClientTest extends AbstractApiClientTest { @Test public void testAssetProfileLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java index 6f79f0d347..d17dd9d0f6 100644 --- a/application/src/test/java/org/thingsboard/server/client/CalculatedFieldJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java @@ -46,7 +46,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class CalculatedFieldJavaClientTest extends AbstractJavaClientTest { +public class CalculatedFieldApiClientTest extends AbstractApiClientTest { @Test public void testCalculatedFieldLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java similarity index 97% rename from application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java index dbfe1faf56..fff3670c85 100644 --- a/application/src/test/java/org/thingsboard/server/client/CustomerJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java @@ -30,7 +30,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class CustomerJavaClientTest extends AbstractJavaClientTest { +public class CustomerApiClientTest extends AbstractApiClientTest { @Test public void testCustomerLifecycle() throws Exception { @@ -52,7 +52,7 @@ public class CustomerJavaClientTest extends AbstractJavaClientTest { createdCustomers.add(createdCustomer); } - // find all, check count (includes savedClientCustomer from AbstractJavaClientTest setup) + // find all, check count (includes savedClientCustomer from AbstractApiClientTest setup) PageDataCustomer allCustomers = client.getCustomers(100, 0, null, null, null); assertNotNull(allCustomers); assertNotNull(allCustomers.getData()); diff --git a/application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java index 5d1fa871c8..386e2db84f 100644 --- a/application/src/test/java/org/thingsboard/server/client/DashboardJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java @@ -29,7 +29,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class DashboardJavaClientTest extends AbstractJavaClientTest { +public class DashboardApiClientTest extends AbstractApiClientTest { @Test public void testDashboardLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java index c141565c42..f3695b9fc1 100644 --- a/application/src/test/java/org/thingsboard/server/client/DeviceJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java @@ -31,7 +31,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class DeviceJavaClientTest extends AbstractJavaClientTest { +public class DeviceApiClientTest extends AbstractApiClientTest { @Test public void testDeviceLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java similarity index 96% rename from application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java index 2ca2d622e7..d8204cab41 100644 --- a/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java @@ -25,7 +25,7 @@ import java.util.UUID; import static org.junit.Assert.assertEquals; @DaoSqlTest -public class DeviceConnectivityJavaClientTest extends AbstractJavaClientTest { +public class DeviceConnectivityApiClientTest extends AbstractApiClientTest { @Test public void testGetDevicePublishTelemetryCommands() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java index 6008c85653..e8fcb4ffd1 100644 --- a/application/src/test/java/org/thingsboard/server/client/DeviceProfileJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java @@ -38,7 +38,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class DeviceProfileJavaClientTest extends AbstractJavaClientTest { +public class DeviceProfileApiClientTest extends AbstractApiClientTest { @Test public void testDeviceProfileLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java index fb8bb9b44f..bd10a3deca 100644 --- a/application/src/test/java/org/thingsboard/server/client/DomainJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java @@ -31,7 +31,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class DomainJavaClientTest extends AbstractJavaClientTest { +public class DomainApiClientTest extends AbstractApiClientTest { List createdDomains = new ArrayList<>(); diff --git a/application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java index 1400ead0b1..6aeb7f0ef6 100644 --- a/application/src/test/java/org/thingsboard/server/client/EdgeJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java @@ -30,7 +30,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class EdgeJavaClientTest extends AbstractJavaClientTest { +public class EdgeApiClientTest extends AbstractApiClientTest { @Test public void testEdgeLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java index 5c075f0777..2571306eb6 100644 --- a/application/src/test/java/org/thingsboard/server/client/EntityQueryJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java @@ -49,7 +49,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class EntityQueryJavaClientTest extends AbstractJavaClientTest { +public class EntityQueryApiClientTest extends AbstractApiClientTest { private static final String QUERY_TEST_PREFIX = "QueryTest_"; diff --git a/application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java index 5113dc380b..1792ba9ff1 100644 --- a/application/src/test/java/org/thingsboard/server/client/EntityRelationJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java @@ -35,7 +35,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class EntityRelationJavaClientTest extends AbstractJavaClientTest { +public class EntityRelationApiClientTest extends AbstractApiClientTest { @Test public void testEntityRelationLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java index 453420f784..60c0e86662 100644 --- a/application/src/test/java/org/thingsboard/server/client/EntityViewJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java @@ -36,7 +36,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class EntityViewJavaClientTest extends AbstractJavaClientTest { +public class EntityViewApiClientTest extends AbstractApiClientTest { private static final String EV_PREFIX = "EvTest_"; diff --git a/application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java index f3f8c2900d..fd448eaee1 100644 --- a/application/src/test/java/org/thingsboard/server/client/MobileAppJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java @@ -33,7 +33,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class MobileAppJavaClientTest extends AbstractJavaClientTest { +public class MobileAppApiClientTest extends AbstractApiClientTest { @Test public void testMobileAppLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java index 027849d153..026b8784ef 100644 --- a/application/src/test/java/org/thingsboard/server/client/NotificationJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java @@ -48,7 +48,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class NotificationJavaClientTest extends AbstractJavaClientTest { +public class NotificationApiClientTest extends AbstractApiClientTest { @Test public void testNotificationLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java index 835b5fc64e..63f05710ae 100644 --- a/application/src/test/java/org/thingsboard/server/client/Oauth2JavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java @@ -34,7 +34,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class Oauth2JavaClientTest extends AbstractJavaClientTest { +public class Oauth2ApiClientTest extends AbstractApiClientTest { private OAuth2Client createOAuth2Client(String title, String clientId, String clientSecret) { OAuth2BasicMapperConfig basicConfig = new OAuth2BasicMapperConfig(); diff --git a/application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java index ebd6413b65..c71f2cb2e3 100644 --- a/application/src/test/java/org/thingsboard/server/client/OtaPackageJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java @@ -37,7 +37,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class OtaPackageJavaClientTest extends AbstractJavaClientTest { +public class OtaPackageApiClientTest extends AbstractApiClientTest { private static final String OTA_PREFIX = "OtaTest_"; diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java similarity index 97% rename from application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java index 399ecd4df9..80719052d1 100644 --- a/application/src/test/java/org/thingsboard/server/client/RpcV1JavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java @@ -23,7 +23,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import static org.junit.Assert.assertEquals; @DaoSqlTest -public class RpcV1JavaClientTest extends AbstractJavaClientTest { +public class RpcV1ApiClientTest extends AbstractApiClientTest { private static final String ONE_WAY_BODY = "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}"; diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java index 43669c1620..02b7e0f0ba 100644 --- a/application/src/test/java/org/thingsboard/server/client/RpcV2JavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java @@ -32,7 +32,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class RpcV2JavaClientTest extends AbstractJavaClientTest { +public class RpcV2ApiClientTest extends AbstractApiClientTest { private static final String PERSISTENT_BODY = "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}"; diff --git a/application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java index 95b5301d9f..8adcecaf24 100644 --- a/application/src/test/java/org/thingsboard/server/client/RuleChainJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java @@ -33,7 +33,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class RuleChainJavaClientTest extends AbstractJavaClientTest { +public class RuleChainApiClientTest extends AbstractApiClientTest { @Test public void testRuleChainAndNodeLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java index 5dbe358991..2a06549554 100644 --- a/application/src/test/java/org/thingsboard/server/client/TbImageJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java @@ -36,7 +36,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class TbImageJavaClientTest extends AbstractJavaClientTest { +public class TbImageApiClientTest extends AbstractApiClientTest { private File createTempImage(String name, Color color) throws IOException { BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); diff --git a/application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java index 156667fecd..0651ce6cc3 100644 --- a/application/src/test/java/org/thingsboard/server/client/TbResourceJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java @@ -33,7 +33,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class TbResourceJavaClientTest extends AbstractJavaClientTest { +public class TbResourceApiClientTest extends AbstractApiClientTest { @Test public void testResourceLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java index 8028c66682..a2dbe0eda4 100644 --- a/application/src/test/java/org/thingsboard/server/client/TelemetryJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java @@ -30,7 +30,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class TelemetryJavaClientTest extends AbstractJavaClientTest { +public class TelemetryApiClientTest extends AbstractApiClientTest { @Test public void testTelemetryLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java index 4ab0f2710e..150486eb8c 100644 --- a/application/src/test/java/org/thingsboard/server/client/TenantJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java @@ -32,7 +32,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class TenantJavaClientTest extends AbstractJavaClientTest { +public class TenantApiClientTest extends AbstractApiClientTest { @Test public void testTenantLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java index e8c110a004..68dd5f7c0e 100644 --- a/application/src/test/java/org/thingsboard/server/client/TenantProfileJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java @@ -35,7 +35,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class TenantProfileJavaClientTest extends AbstractJavaClientTest { +public class TenantProfileApiClientTest extends AbstractApiClientTest { @Test public void testTenantProfileLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java similarity index 97% rename from application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java index 972049d44c..5f371b5d82 100644 --- a/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java @@ -33,7 +33,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @DaoSqlTest -public class TwoFactorAuthJavaClientTest extends AbstractJavaClientTest { +public class TwoFactorAuthApiClientTest extends AbstractApiClientTest { @Test public void testTwoFactorAuthLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java index 378f48846f..154858405f 100644 --- a/application/src/test/java/org/thingsboard/server/client/UserJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java @@ -31,7 +31,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class UserJavaClientTest extends AbstractJavaClientTest { +public class UserApiClientTest extends AbstractApiClientTest { @Test public void testUserLifecycle() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java b/application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java similarity index 98% rename from application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java rename to application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java index 503c8efcf5..ee3984bb4b 100644 --- a/application/src/test/java/org/thingsboard/server/client/WidgetTypeJavaClientTest.java +++ b/application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java @@ -31,7 +31,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @DaoSqlTest -public class WidgetTypeJavaClientTest extends AbstractJavaClientTest { +public class WidgetTypeApiClientTest extends AbstractApiClientTest { private JsonNode createDescriptor(String type) { return OBJECT_MAPPER.createObjectNode() From 0779f9502fc249b8195f521e085d471da90c8f86 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 18 Mar 2026 15:24:42 +0200 Subject: [PATCH 170/313] Add ApiKeyApiClientTest --- .../server/client/ApiKeyApiClientTest.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java diff --git a/application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java new file mode 100644 index 0000000000..4b774a442a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java @@ -0,0 +1,117 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.client; + +import org.junit.Test; +import org.thingsboard.client.model.ApiKey; +import org.thingsboard.client.model.ApiKeyInfo; +import org.thingsboard.client.model.PageDataApiKeyInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class ApiKeyApiClientTest extends AbstractApiClientTest { + + @Test + public void testApiKeyLifecycle() throws Exception { + String userId = clientTenantAdmin.getId().getId().toString(); + + ApiKeyInfo request = new ApiKeyInfo(); + request.setDescription("Test API key"); + request.setUserId(clientTenantAdmin.getId()); + request.setEnabled(true); + ApiKey created = client.saveApiKey(request); + + assertNotNull(created); + assertNotNull(created.getId()); + assertNotNull(created.getValue()); + assertFalse(created.getValue().isBlank()); + assertEquals("Test API key", created.getDescription()); + + UUID keyId = created.getId().getId(); + + PageDataApiKeyInfo keysPage = client.getUserApiKeys(userId, 100, 0, null, null, null); + assertNotNull(keysPage); + assertNotNull(keysPage.getData()); + assertTrue("Newly created API key should appear in user's key list", + keysPage.getData().stream() + .anyMatch(k -> k.getId().getId().equals(keyId))); + + client.deleteApiKey(keyId); + + PageDataApiKeyInfo keysAfterDelete = client.getUserApiKeys(userId, 100, 0, null, null, null); + assertTrue("Deleted API key should not appear in user's key list", + keysAfterDelete.getData().stream() + .noneMatch(k -> k.getId().getId().equals(keyId))); + } + + @Test + public void testEnableDisableApiKey() throws Exception { + ApiKeyInfo request = new ApiKeyInfo(); + request.setDescription("Enable/disable test key"); + request.setUserId(clientTenantAdmin.getId()); + request.setEnabled(true); + ApiKey created = client.saveApiKey(request); + assertNotNull(created); + + UUID keyId = created.getId().getId(); + + ApiKeyInfo disabled = client.enableApiKey(keyId, false); + assertNotNull(disabled); + assertEquals(Boolean.FALSE, disabled.getEnabled()); + + ApiKeyInfo enabled = client.enableApiKey(keyId, true); + assertNotNull(enabled); + assertEquals(Boolean.TRUE, enabled.getEnabled()); + + client.deleteApiKey(keyId); + } + + @Test + public void testGetUserApiKeys() throws Exception { + String userId = clientTenantAdmin.getId().getId().toString(); + + int initialCount = client.getUserApiKeys(userId, 100, 0, null, null, null) + .getData().size(); + + UUID[] createdIds = new UUID[3]; + for (int i = 0; i < 3; i++) { + ApiKeyInfo request = new ApiKeyInfo(); + request.setDescription("Paging test key " + i); + request.setUserId(clientTenantAdmin.getId()); + createdIds[i] = client.saveApiKey(request).getId().getId(); + } + + PageDataApiKeyInfo afterCreate = client.getUserApiKeys(userId, 100, 0, null, null, null); + assertEquals(initialCount + 3, afterCreate.getData().size()); + assertEquals(Long.valueOf(initialCount + 3), afterCreate.getTotalElements()); + + PageDataApiKeyInfo page1 = client.getUserApiKeys(userId, 2, 0, null, null, null); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + for (UUID id : createdIds) { + client.deleteApiKey(id); + } + } + +} From ffde28eeb56e23fb0a128ced8dc078e60da1661b Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Wed, 18 Mar 2026 16:24:44 +0200 Subject: [PATCH 171/313] Remove redurant multiple locale entries from Korean JSON --- .../assets/locale/locale.constant-ko_KR.json | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index ab9432a169..0351963ed9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -10209,36 +10209,6 @@ }, "language": { "auto": "자동", - "language": "언어(Language)", - "locales": { - "ar_AE": "العربية (الإمارات العربية المتحدة)", - "ca_ES": "català (Espanya)", - "cs_CZ": "čeština (Česko)", - "da_DK": "dansk (Danmark)", - "de_DE": "Deutsch (Deutschland)", - "el_GR": "Ελληνικά (Ελλάδα)", - "en_US": "English (United States)", - "es_ES": "español (España)", - "fa_IR": "فارسی (ایران)", - "fr_FR": "français (France)", - "hi_IN": "हिन्दी (भारत)", - "it_IT": "italiano (Italia)", - "ja_JP": "日本語 (日本)", - "ka_GE": "ქართული (საქართველო)", - "ko_KR": "한국어 (대한민국)", - "lt_LT": "lietuvių (Lietuva)", - "lv_LV": "latviešu (Latvija)", - "nl_BE": "Nederlands (België)", - "nl_NL": "Nederlands (Nederland)", - "no_NO": "norsk (Norge)", - "pl_PL": "polski (Polska)", - "pt_BR": "português (Brasil)", - "ro_RO": "română (România)", - "sl_SI": "slovenščina (Slovenija)", - "tr_TR": "Türkçe (Türkiye)", - "uk_UA": "українська (Україна)", - "zh_CN": "中文 (中国)", - "zh_TW": "中文 (台灣)" - } + "language": "언어(Language)" } } From a425e35e603b0ca939c6bebffb57ddf70a2c8cb9 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 18 Mar 2026 17:08:08 +0100 Subject: [PATCH 172/313] Missed input --- .../home/pages/admin/resource/js-resource.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html index 71de635900..009e549554 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html @@ -118,7 +118,7 @@ } @if (!isAdd && !isEdit && entityForm.get('resourceSubType').value === ResourceSubType.EXTENSION) {
- + resource.file-name From 3985bf4a21fba403f67b45c450e8a35738562997 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Sat, 21 Mar 2026 13:08:22 +0200 Subject: [PATCH 173/313] Add CLAUDE.md for UI module --- ui-ngx/CLAUDE.md | 742 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 742 insertions(+) create mode 100644 ui-ngx/CLAUDE.md diff --git a/ui-ngx/CLAUDE.md b/ui-ngx/CLAUDE.md new file mode 100644 index 0000000000..0ce231c91c --- /dev/null +++ b/ui-ngx/CLAUDE.md @@ -0,0 +1,742 @@ +# ThingsBoard CE Frontend — Navigation & Testing Guide + +## 1. Quick Reference + +| Key | Value | +|-----|-------| +| Angular | 20 | +| Angular Material | 20 | +| Component prefix | `tb` | +| TypeScript | 5.9 | +| RxJS | 7.8 | + +**Path Aliases** (from `tsconfig.json`): + +| Alias | Maps to | +|-------|---------| +| `@app/*` | `src/app/*` | +| `@core/*` | `src/app/core/*` | +| `@modules/*` | `src/app/modules/*` | +| `@home/*` | `src/app/modules/home/*` | +| `@shared/*` | `src/app/shared/*` | + +**Key Directories:** + +``` +src/app/ +├── core/ +│ ├── http/ # HTTP services (one per entity type) +│ └── services/ # Menu, auth, utils +├── modules/ +│ ├── home/ +│ │ ├── components/ # Reusable home components (entity table, details panel, alarm, dashboard) +│ │ │ └── entity/ # EntitiesTableComponent, EntityDetailsPageComponent, EntityComponent base +│ │ └── pages/ # Page modules (one directory per page/feature) +│ └── login/ # Login, reset password, 2FA +└── shared/ + ├── components/ # Shared components (autocompletes, dialogs, time, image) + └── models/ # TypeScript interfaces/enums +``` + +--- + +## 2. Route-to-Source Mapping + +All page source paths are relative to `src/app/modules/home/pages/`. +Page types: `entity-table` = EntitiesTableComponent + resolver, `entity-details` = EntityDetailsPageComponent, `settings-form` = standalone form with ConfirmOnExitGuard, `dashboard-view` = DashboardViewComponent with resolver, `custom` = unique component. + +### Login (unauthenticated) + +Source paths below are relative to `src/app/modules/` (not `home/pages/`). + +| URL | Source | Type | +|-----|--------|------| +| `/login` | `login/pages/login/` | custom (LoginComponent) | +| `/login/resetPasswordRequest` | `login/pages/login/` | custom (ResetPasswordRequestComponent) | +| `/login/resetPassword` | `login/pages/login/` | custom (ResetPasswordComponent) | +| `/login/createPassword` | `login/pages/login/` | custom (CreatePasswordComponent) | +| `/login/mfa` | `login/pages/login/` | custom (TwoFactorAuthLoginComponent) | + +### Home + +| URL | Source | Type | Auth | +|-----|--------|------|------| +| `/home` | `home-links/` | custom (HomeLinksComponent + dashboard resolver) | ALL | + +### Tenants (SYS_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/tenants` | `tenant/` | entity-table | `TenantsTableConfigResolver` | +| `/tenants/:entityId` | `tenant/` | entity-details | `TenantsTableConfigResolver` | +| `/tenants/:tenantId/users` | `tenant/` + `user/` | entity-table | `UsersTableConfigResolver` | +| `/tenantProfiles` | `tenant-profile/` | entity-table | `TenantProfilesTableConfigResolver` | +| `/tenantProfiles/:entityId` | `tenant-profile/` | entity-details | `TenantProfilesTableConfigResolver` | + +### Alarms Center (TENANT_ADMIN, CUSTOMER_USER) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/alarms` | `alarm/` | custom (RouterTabsComponent) | — | +| `/alarms/alarms` | `alarm/` | custom (AlarmTableComponent) | — | +| `/alarms/alarm-rules` | `alarm/` | custom (AlarmRulesTableComponent) | — | +| `/alarms/alarm-rules/:entityId` | `alarm/` | entity-details | `AlarmRulesTableConfigResolver` | + +### Dashboards (TENANT_ADMIN, CUSTOMER_USER) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/dashboards` | `dashboard/` | entity-table | `DashboardsTableConfigResolver` | +| `/dashboards/:dashboardId` | `dashboard/` | custom (DashboardPageComponent) | `DashboardResolver` | + +### Entities (TENANT_ADMIN, CUSTOMER_USER) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/entities/devices` | `device/` | entity-table | `DevicesTableConfigResolver` | +| `/entities/devices/:entityId` | `device/` | entity-details | `DevicesTableConfigResolver` | +| `/entities/assets` | `asset/` | entity-table | `AssetsTableConfigResolver` | +| `/entities/assets/:entityId` | `asset/` | entity-details | `AssetsTableConfigResolver` | +| `/entities/entityViews` | `entity-view/` | entity-table | `EntityViewsTableConfigResolver` | +| `/entities/entityViews/:entityId` | `entity-view/` | entity-details | `EntityViewsTableConfigResolver` | +| `/entities/gateways` | `gateways/` | dashboard-view | `gatewaysDashboardResolver` | + +### Profiles (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/profiles/deviceProfiles` | `device-profile/` | entity-table | `DeviceProfilesTableConfigResolver` | +| `/profiles/deviceProfiles/:entityId` | `device-profile/` | entity-details | `DeviceProfilesTableConfigResolver` | +| `/profiles/assetProfiles` | `asset-profile/` | entity-table | `AssetProfilesTableConfigResolver` | +| `/profiles/assetProfiles/:entityId` | `asset-profile/` | entity-details | `AssetProfilesTableConfigResolver` | + +### Customers (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/customers` | `customer/` | entity-table | `CustomersTableConfigResolver` | +| `/customers/:entityId` | `customer/` | entity-details | `CustomersTableConfigResolver` | +| `/customers/:customerId/users` | `customer/` + `user/` | entity-table | `UsersTableConfigResolver` | +| `/customers/:customerId/devices` | `customer/` + `device/` | entity-table | `DevicesTableConfigResolver` | +| `/customers/:customerId/assets` | `customer/` + `asset/` | entity-table | `AssetsTableConfigResolver` | +| `/customers/:customerId/dashboards` | `customer/` + `dashboard/` | entity-table | `DashboardsTableConfigResolver` | +| `/customers/:customerId/edgeInstances` | `customer/` + `edge/` | entity-table | `EdgesTableConfigResolver` | + +### Calculated Fields (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/calculatedFields` | `calculated-fields/` | custom (CalculatedFieldsTableComponent) | — | +| `/calculatedFields/:entityId` | `calculated-fields/` | entity-details | `CalculatedFieldsTableConfigResolver` | + +### Rule Chains (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/ruleChains` | `rulechain/` | entity-table | `RuleChainsTableConfigResolver` | +| `/ruleChains/:ruleChainId` | `rulechain/` | custom (RuleChainPageComponent, lazy) | `RuleChainResolver` + others | + +### Edge Management (TENANT_ADMIN, conditional on edgesSupportEnabled) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/edgeManagement/instances` | `edge/` | entity-table | `EdgesTableConfigResolver` | +| `/edgeManagement/instances/:entityId` | `edge/` | entity-details | `EdgesTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/assets` | `edge/` + `asset/` | entity-table | `AssetsTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/devices` | `edge/` + `device/` | entity-table | `DevicesTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/entityViews` | `edge/` + `entity-view/` | entity-table | `EntityViewsTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/dashboards` | `edge/` + `dashboard/` | entity-table | `DashboardsTableConfigResolver` | +| `/edgeManagement/instances/:edgeId/ruleChains` | `edge/` + `rulechain/` | entity-table | `RuleChainsTableConfigResolver` | +| `/edgeManagement/ruleChains` | `edge/` + `rulechain/` | entity-table | `RuleChainsTableConfigResolver` | + +### Advanced Features (TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/features/otaUpdates` | `ota-update/` | entity-table | `OtaUpdateTableConfigResolve` | +| `/features/otaUpdates/:entityId` | `ota-update/` | entity-details | `OtaUpdateTableConfigResolve` | +| `/features/vc` | `vc/` | custom (VersionControlComponent) | — | + +### Resources (SYS_ADMIN, TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/resources/widgets-library/widget-types` | `widget/` | entity-table | `WidgetTypesTableConfigResolver` | +| `/resources/widgets-library/widgets-bundles` | `widget/` | entity-table | `WidgetsBundlesTableConfigResolver` | +| `/resources/widgets-library/widgets-bundles/:widgetsBundleId` | `widget/` | custom (WidgetsBundleWidgetsComponent) | — | +| `/resources/widgets-library/widget-types/:widgetTypeId` | `widget/` | custom (WidgetEditorComponent) | — | +| `/resources/images` | `admin/` | custom (ImageGalleryComponent) | — | +| `/resources/scada-symbols` | `admin/` | custom (ImageGalleryComponent) | — | +| `/resources/scada-symbols/:type/:key` | `admin/` | custom (ScadaSymbolComponent) | — | +| `/resources/resources-library` | `admin/` | entity-table | `ResourcesLibraryTableConfigResolver` | +| `/resources/resources-library/:entityId` | `admin/` | entity-details | `ResourcesLibraryTableConfigResolver` | +| `/resources/javascript-library` | `admin/` | entity-table | `JsLibraryTableConfigResolver` | +| `/resources/javascript-library/:entityId` | `admin/` | entity-details | `JsLibraryTableConfigResolver` | + +### Notifications (ALL authorities) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/notification/inbox` | `notification/inbox/` | entity-table | `InboxTableConfigResolver` | +| `/notification/sent` | `notification/sent/` | entity-table | `SentTableConfigResolver` | +| `/notification/recipients` | `notification/recipient/` | entity-table | `RecipientTableConfigResolver` | +| `/notification/templates` | `notification/template/` | entity-table | `TemplateTableConfigResolver` | +| `/notification/rules` | `notification/rule/` | entity-table | `RuleTableConfigResolver` | + +### Mobile Center (SYS_ADMIN, TENANT_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/mobile-center/bundles` | `mobile/bundes/` | entity-table | `MobileBundleTableConfigResolver` | +| `/mobile-center/applications` | `mobile/applications/` | entity-table | `MobileAppTableConfigResolver` | +| `/mobile-center/applications/:entityId` | `mobile/applications/` | entity-details | `MobileAppTableConfigResolver` | +| `/mobile-center/qr-code-widget` | `mobile/qr-code-widget/` | settings-form | — | + +### Settings — SYS_ADMIN + +| URL | Source | Type | +|-----|--------|------| +| `/settings/general` | `admin/` | settings-form (GeneralSettingsComponent) | +| `/settings/outgoing-mail` | `admin/` | settings-form (MailServerComponent) | +| `/settings/notifications` | `admin/` | settings-form (SmsProviderComponent) | +| `/settings/queues` | `admin/` | entity-table (`QueuesTableConfigResolver`) | + +### Settings — TENANT_ADMIN + +| URL | Source | Type | +|-----|--------|------| +| `/settings/home` | `admin/` | settings-form (HomeSettingsComponent) | +| `/settings/notifications` | `admin/` | settings-form (SmsProviderComponent) | +| `/settings/repository` | `admin/` | settings-form (RepositoryAdminSettingsComponent) | +| `/settings/auto-commit` | `admin/` | settings-form (AutoCommitAdminSettingsComponent) | +| `/settings/trendz` | `admin/` | settings-form (TrendzSettingsComponent) | +| `/settings/ai-models` | `ai-model/` | entity-table (`AiModelsTableConfigResolver`) | + +### Security Settings (SYS_ADMIN) + +| URL | Source | Type | Resolver | +|-----|--------|------|----------| +| `/security-settings/general` | `admin/` | settings-form (SecuritySettingsComponent) | — | +| `/security-settings/2fa` | `admin/` | settings-form (TwoFactorAuthSettingsComponent) | — | +| `/security-settings/oauth2/domains` | `admin/` | entity-table | `DomainTableConfigResolver` | +| `/security-settings/oauth2/clients` | `admin/` | entity-table | `ClientsTableConfigResolver` | +| `/security-settings/auditLogs` | `admin/` | custom (audit log table) | — | + +### Account (ALL authorities) + +| URL | Source | Type | +|-----|--------|------| +| `/account/profile` | `profile/` | settings-form (ProfileComponent) | +| `/account/security` | `security/` | settings-form (SecurityComponent) | + +### API Usage (TENANT_ADMIN) + +| URL | Source | Type | +|-----|--------|------| +| `/usage` | `api-usage/` | dashboard-view (loads `api_usage.json`) | + +--- + +## 3. Entity Table Pattern + +Most pages follow a 3-file pattern per entity type inside `pages//`: + +``` +-table-config.resolver.ts — Column definitions, actions, component wiring +.component.ts + .html — Entity detail form (extends EntityComponent) +-tabs.component.ts + .html — Additional detail tabs (relations, attributes, events, etc.) +``` + +**Framework files** (in `home/components/entity/`): +- `entities-table.component.ts` — The generic table component. Renders columns, actions, pagination from config. +- `entity-details-page.component.ts` — Full-page entity details with tabs. +- `entity-details-panel.component.ts` — Slide-out drawer panel for entity details. +- `entity.component.ts` — Abstract base directive all entity forms extend. +- `entities-table-config.models.ts` — TypeScript interfaces for table configuration. + +**Resolver structure** — each resolver returns an `EntityTableConfig` with: +- `entityType` — enum value (e.g., `EntityType.DEVICE`) +- `columns` — array of column definitions (header key, cell property/template, width) +- `cellActionDescriptors` — per-row action buttons +- `groupActionDescriptors` — bulk-selection action buttons +- `headerActionDescriptors` — toolbar action buttons (add, import) +- `entityComponent` — reference to the detail form component class +- `entityTabsComponent` — reference to the tabs component class +- `addDialogComponent` — optional dialog for add flow (e.g., `DeviceWizardDialogComponent`) + +### All Table Config Resolvers + +| Resolver | Entity Type | Source Dir | Entity Component | Tabs Component | +|----------|-------------|-----------|------------------|----------------| +| `TenantsTableConfigResolver` | TENANT | `tenant/` | `TenantComponent` | `TenantTabsComponent` | +| `TenantProfilesTableConfigResolver` | TENANT_PROFILE | `tenant-profile/` | `TenantProfileComponent` | `TenantProfileTabsComponent` | +| `DevicesTableConfigResolver` | DEVICE | `device/` | `DeviceComponent` | `DeviceTabsComponent` | +| `DeviceProfilesTableConfigResolver` | DEVICE_PROFILE | `device-profile/` | `DeviceProfileComponent` | `DeviceProfileTabsComponent` | +| `AssetsTableConfigResolver` | ASSET | `asset/` | `AssetComponent` | `AssetTabsComponent` | +| `AssetProfilesTableConfigResolver` | ASSET_PROFILE | `asset-profile/` | `AssetProfileComponent` | `AssetProfileTabsComponent` | +| `EntityViewsTableConfigResolver` | ENTITY_VIEW | `entity-view/` | `EntityViewComponent` | `EntityViewTabsComponent` | +| `CustomersTableConfigResolver` | CUSTOMER | `customer/` | `CustomerComponent` | `CustomerTabsComponent` | +| `UsersTableConfigResolver` | USER | `user/` | `UserComponent` | `UserTabsComponent` | +| `DashboardsTableConfigResolver` | DASHBOARD | `dashboard/` | — | — | +| `EdgesTableConfigResolver` | EDGE | `edge/` | `EdgeComponent` | `EdgeTabsComponent` | +| `RuleChainsTableConfigResolver` | RULE_CHAIN | `rulechain/` | `RuleChainComponent` | `RuleChainTabsComponent` | +| `OtaUpdateTableConfigResolve` | OTA_PACKAGE | `ota-update/` | — | — | +| `CalculatedFieldsTableConfigResolver` | CALCULATED_FIELD | `calculated-fields/` | — | — | +| `WidgetTypesTableConfigResolver` | WIDGETS_BUNDLE | `widget/` | `WidgetTypeComponent` | `WidgetTypeTabsComponent` | +| `WidgetsBundlesTableConfigResolver` | WIDGETS_BUNDLE | `widget/` | `WidgetsBundleComponent` | `WidgetsBundleTabsComponent` | +| `ResourcesLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | — | — | +| `JsLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | — | — | +| `QueuesTableConfigResolver` | QUEUE | `admin/queue/` | `QueueComponent` | — | +| `AiModelsTableConfigResolver` | AI_MODEL | `ai-model/` | — | — | +| `ClientsTableConfigResolver` | OAUTH2_CLIENT | `admin/oauth2/clients/` | `ClientComponent` | — | +| `InboxTableConfigResolver` | NOTIFICATION | `notification/inbox/` | — (dialog) | — | +| `SentTableConfigResolver` | NOTIFICATION_REQUEST | `notification/sent/` | — (dialog) | — | +| `RecipientTableConfigResolver` | NOTIFICATION_TARGET | `notification/recipient/` | — (dialog) | — | +| `TemplateTableConfigResolver` | NOTIFICATION_TEMPLATE | `notification/template/` | — (dialog) | — | +| `RuleTableConfigResolver` | NOTIFICATION_RULE | `notification/rule/` | — (dialog) | — | +| `MobileAppTableConfigResolver` | MOBILE_APP | `mobile/applications/` | `MobileAppComponent` | — | +| `MobileBundleTableConfigResolver` | MOBILE_BUNDLE | `mobile/bundes/` | — | — | +| `AlarmRulesTableConfigResolver` | CALCULATED_FIELD | `alarm/` (config in `home/components/alarm-rules/`) | — | — | +| `DomainTableConfigResolver` | DOMAIN | `admin/oauth2/domains/` | `DomainComponent` | — | + +--- + +## 4. Shared Component Catalog + +### `@shared/components/entity/` + +| Selector | Purpose | +|----------|---------| +| `tb-entity-autocomplete` | Autocomplete input for selecting a single entity | +| `tb-entity-select` | Dropdown select for entity | +| `tb-entity-list` | Chip list for multiple entities | +| `tb-entity-list-select` | Dropdown with entity list selection | +| `tb-entity-key-autocomplete` | Autocomplete for entity attribute/telemetry keys | +| `tb-entity-keys-list` | Chip list for multiple entity keys | +| `tb-entity-type-select` | Dropdown for entity type enum | +| `tb-entity-type-list` | Chip list for multiple entity types | +| `tb-entity-subtype-select` | Dropdown for entity subtype (e.g., device type) | +| `tb-entity-subtype-list` | Chip list for multiple subtypes | +| `tb-entity-subtype-autocomplete` | Autocomplete for entity subtype | +| `tb-entity-gateway-select` | Dropdown for selecting a gateway device | + +### `@shared/components/time/` + +| Selector | Purpose | +|----------|---------| +| `tb-timewindow` | Timewindow selector (realtime/history) | +| `tb-timewindow-panel` | Expanded panel for timewindow config | +| `tb-timewindow-config-dialog` | Dialog for advanced timewindow settings | +| `tb-datetime` | Date-time picker input | +| `tb-datetime-period` | Date range picker | +| `tb-timezone` | Timezone display | +| `tb-timezone-select` | Dropdown for timezone selection | +| `tb-quick-time-interval` | Quick interval preset selector | +| `tb-timeinterval` | Duration input (number + unit) | +| `tb-datapoints-limit` | Numeric input for data point limits | +| `tb-aggregation-type-select` | Dropdown for aggregation type (AVG, SUM, etc.) | +| `tb-history-selector` | History navigation selector | + +### `@shared/components/image/` + +| Selector | Purpose | +|----------|---------| +| `tb-image-gallery` | Grid gallery of uploaded images | +| `tb-image-gallery-dialog` | Dialog wrapper for image gallery | +| `tb-image-input` | Single image upload input | +| `tb-gallery-image-input` | Image input that opens gallery for selection | +| `tb-multiple-gallery-image-input` | Multi-image input with gallery | +| `tb-upload-image-dialog` | Upload dialog | +| `tb-embed-image-dialog` | Embed external image dialog | +| `tb-scada-symbol-input` | SCADA symbol selector input | + +### `@shared/components/dialog/` + +| Selector | Purpose | +|----------|---------| +| `tb-confirm-dialog` | Yes/No confirmation dialog | +| `tb-alert-dialog` | Information alert dialog | +| `tb-error-alert-dialog` | Error message dialog | +| `tb-color-picker-dialog` | Color picker in a dialog | +| `tb-material-icons-dialog` | Material icon browser dialog | +| `tb-node-script-test-dialog` | Script testing dialog for rule nodes | +| `tb-object-edit-dialog` | JSON object editor dialog | +| `tb-todo-dialog` | Todo/checklist dialog | + +### `@shared/components/` (other) + +| Selector | Purpose | +|----------|---------| +| `tb-json-content` | JSON syntax-highlighted viewer | +| `tb-json-object-edit` | Editable JSON object component | +| `tb-json-object-view` | Read-only JSON object display | +| `tb-markdown` | Markdown editor/viewer | +| `tb-breadcrumb` | Breadcrumb navigation bar | +| `tb-icon` | Custom icon component | +| `tb-logo` | ThingsBoard logo | +| `tb-copy-button` | Copy-to-clipboard button | +| `tb-toggle-password` | Password visibility toggle | +| `tb-file-input` | File upload input | +| `tb-phone-input` | International phone input | +| `tb-unit-input` | Measurement unit input | +| `tb-value-input` | Generic value input with type selection | +| `tb-color-input` | Color picker input | +| `tb-color-picker` | Inline color picker | +| `tb-key-val-map` | Key-value pair map editor | +| `tb-nav-tree` | Tree navigation component | +| `tb-user-menu` | User profile dropdown menu | +| `tb-notification-bell` | Notification bell icon with badge | +| `tb-footer` | Page footer | +| `tb-scroll-grid` | Virtual-scroll grid | +| `tb-social-share-panel` | Social sharing buttons | + +### Autocomplete / Select Components + +| Selector | Purpose | +|----------|---------| +| `tb-dashboard-autocomplete` | Autocomplete for dashboard selection | +| `tb-dashboard-select` | Dropdown for dashboard | +| `tb-dashboard-state-autocomplete` | Autocomplete for dashboard states | +| `tb-country-autocomplete` | Country autocomplete | +| `tb-string-autocomplete` | Generic string autocomplete | +| `tb-message-type-autocomplete` | Rule engine message type autocomplete | +| `tb-queue-autocomplete` | Queue name autocomplete | +| `tb-relation-type-autocomplete` | Relation type autocomplete | +| `tb-resource-autocomplete` | Resource autocomplete | +| `tb-ota-package-autocomplete` | OTA package autocomplete | +| `tb-branch-autocomplete` | Git branch autocomplete | +| `tb-rule-chain-select` | Rule chain select dropdown | +| `tb-widgets-bundle-select` | Widgets bundle select dropdown | +| `tb-template-autocomplete` | Notification template autocomplete | +| `tb-material-icon-select` | Material icon picker | +| `tb-slack-conversation-autocomplete` | Slack conversation autocomplete | + +### `@home/components/` (key components) + +| Selector | Purpose | +|----------|---------| +| `tb-entities-table` | Main entity table (columns, sort, pagination, actions) | +| `tb-entity-details-panel` | Slide-out panel for entity details | +| `tb-entity-details-page` | Full-page entity details view | +| `tb-add-entity-dialog` | Generic add-entity dialog | +| `tb-entity-filter` | Entity data filter component | +| `tb-entity-chips` | Chip display for entity references | +| `tb-details-panel` | Base details panel (toolbar + edit/apply FABs) | +| `tb-alarm-table` | Alarm list table | +| `tb-alarm-details-dialog` | Alarm detail dialog with ack/clear | +| `tb-alarm-assignee` | Alarm assignee selector | +| `tb-alarm-comment` | Alarm comment thread | +| `tb-alarm-filter-config` | Alarm filter configuration | +| `tb-audit-log-table` | Audit log table | +| `tb-attribute-table` | Entity attribute key-value table | +| `tb-relation-table` | Entity relation table | +| `tb-event-table` | Entity event table | +| `tb-dashboard` | Dashboard renderer | +| `tb-dashboard-page` | Dashboard page (toolbar + dashboard) | +| `tb-dashboard-toolbar` | Dashboard edit/view toolbar | +| `tb-edit-widget` | Widget editor | +| `tb-router-tabs` | Tab navigation via router (used for section tabs) | +| `tb-device-credentials` | Device credentials form | + +--- + +## 5. Angular Material DOM Patterns + +**No `data-testid` attributes** — selectors must use Angular Material structure, `formControlName`, `matColumnDef`, labels, roles, and classes. + +### Login Form + +```html + + + + + + + + + + + + + + + +``` + +Key selectors: `#username-input`, `#password-input`, `button[type="submit"]`. + +### Main Layout + +```html + + + + + + + + + + + + + + +``` + +Key selectors: `mat-sidenav#sidenav`, `button#main`, `tb-breadcrumb`, `tb-user-menu`. + +### Entity Table + +```html + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+``` + +Key selectors: `table[mat-table]`, `mat-row`, `mat-cell`, `mat-paginator`, `[matColumnDef="select"]`, `[matColumnDef="actions"]`. Note: the search input uses Angular template ref `#searchInput` (not an HTML `id`); target it via `.tb-entity-table mat-toolbar input[matInput]` in the search toolbar context. + +### Entity Details Page + +```html + + + +
+
+ + +
+
+ + + + + + +
+``` + +Key selectors: `mat-toolbar.details-toolbar`, `button[mat-fab][color="accent"]`, `mat-tab-group`, `mat-tab`. + +### Details Panel (Slide-Out Drawer) + +```html +
+ + + +
+ + +
+
+
+
+ +
+``` + +### Entity Detail Form (inside a tab) + +```html +
+
+
+ + + + + + + + + + + + + + +
+
+
+``` + +Key selectors: `input[formControlName="name"]`, `mat-slide-toggle[formControlName="..."]`, `textarea[formControlName="description"]`. + +### Dialog + +```html +
+ +

+ + +
+ +
+ +
+
+ + +
+ +``` + +Key selectors: `[mat-dialog-content]`, `[mat-dialog-actions]`, `button[type="submit"]`, `mat-toolbar[color="primary"] h2`. + +### Common Material Selector Patterns + +``` +# Find a form field by its control name +input[formControlName="name"] +mat-select[formControlName="type"] +mat-slide-toggle[formControlName="gateway"] +textarea[formControlName="description"] + +# Find table column content +[matColumnDef="name"] mat-cell + +# Find buttons by tooltip +button[matTooltip="Search"] + +# Find a specific tab +mat-tab-group mat-tab (by index or label text) + +# Find menu items (after opening menu) +button[mat-menu-item] + +# Find dialog actions +[mat-dialog-actions] button[mat-raised-button] +``` + +--- + +## 6. Form Structure Guide + +All entity detail forms extend the abstract `EntityComponent` directive (`@home/components/entity/entity.component.ts`). + +**Key properties:** +- `entityForm: UntypedFormGroup` — the reactive form instance +- `entity: T` — current entity data (input) +- `isEdit: boolean` — whether form is in edit mode (input) + +**Lifecycle:** + +1. **`buildForm(entity: T): UntypedFormGroup`** — Called in constructor. Creates the FormGroup with controls, validators, and initial values. Subscribe to valueChanges here for cascading updates. + +2. **`updateForm(entity: T): void`** — Called when entity input changes. Uses `entityForm.patchValue({...}, {emitEvent: false})` to populate form without triggering change events. + +3. **`updateFormState(): void`** — Called when `isEdit` changes. Calls `entityForm.enable()` or `entityForm.disable()` with `{emitEvent: false}`. + +4. **`entityFormValue(): any`** — Returns `entityForm.getRawValue()` (reads disabled fields too), passed through `prepareFormValue()` (default: `deepTrim`). + +**Read-only pattern:** Templates use `
` to visually disable all inputs. The form group is also programmatically disabled via `entityForm.disable()`. + +**Saving:** Merges `{...this.entity, ...this.entityFormValue()}` and calls the HTTP service. + +**Common validators:** `Validators.required`, `Validators.maxLength(255)`, custom `validateEmail`. + +**Nested FormGroups** — `additionalInfo` is almost always a nested group: +```typescript +additionalInfo: this.fb.group({ + description: [''], + gateway: [false], + // ... +}) +``` + +--- + +## 7. HTTP Services Reference + +All services are in `src/app/core/http/`. + +| Entity / Area | Service File | Class | +|---------------|-------------|-------| +| Device | `device.service.ts` | `DeviceService` | +| Asset | `asset.service.ts` | `AssetService` | +| Device Profile | `device-profile.service.ts` | `DeviceProfileService` | +| Asset Profile | `asset-profile.service.ts` | `AssetProfileService` | +| Entity View | `entity-view.service.ts` | `EntityViewService` | +| Customer | `customer.service.ts` | `CustomerService` | +| User | `user.service.ts` | `UserService` | +| Tenant | `tenant.service.ts` | `TenantService` | +| Tenant Profile | `tenant-profile.service.ts` | `TenantProfileService` | +| Dashboard | `dashboard.service.ts` | `DashboardService` | +| Rule Chain | `rule-chain.service.ts` | `RuleChainService` | +| Edge | `edge.service.ts` | `EdgeService` | +| Alarm | `alarm.service.ts` | `AlarmService` | +| Alarm Comment | `alarm-comment.service.ts` | `AlarmCommentService` | +| OTA Package | `ota-package.service.ts` | `OtaPackageService` | +| Calculated Fields | `calculated-fields.service.ts` | `CalculatedFieldsService` | +| Widget | `widget.service.ts` | `WidgetService` | +| Resource | `resource.service.ts` | `ResourceService` | +| Queue | `queue.service.ts` | `QueueService` | +| AI Model | `ai-model.service.ts` | `AiModelService` | +| Notification | `notification.service.ts` | `NotificationService` | +| OAuth2 | `oauth2.service.ts` | `OAuth2Service` | +| Domain | `domain.service.ts` | `DomainService` | +| Mobile App | `mobile-app.service.ts` | `MobileAppService` | +| Attribute | `attribute.service.ts` | `AttributeService` | +| Entity Relation | `entity-relation.service.ts` | `EntityRelationService` | +| Entity (generic) | `entity.service.ts` | `EntityService` | +| Event | `event.service.ts` | `EventService` | +| Audit Log | `audit-log.service.ts` | `AuditLogService` | +| Admin Settings | `admin.service.ts` | `AdminService` | +| Version Control | `entities-version-control.service.ts` | `EntitiesVersionControlService` | +| 2FA | `two-factor-authentication.service.ts` | `TwoFactorAuthenticationService` | +| User Settings | `user-settings.service.ts` | `UserSettingsService` | +| API Key | `api-key.service.ts` | `ApiKeyService` | +| Image | `image.service.ts` | `ImageService` | +| Trendz Settings | `trendz-settings.service.ts` | `TrendzSettingsService` | +| Usage Info | `usage-info.service.ts` | `UsageInfoService` | +| Component Descriptor | `component-descriptor.service.ts` | `ComponentDescriptorService` | From 182bbd52d545abcf273a4ba319c522bd8e7d3bc2 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Sat, 21 Mar 2026 13:37:39 +0200 Subject: [PATCH 174/313] Update UI CLAUDE.md with accurate component references --- ui-ngx/CLAUDE.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ui-ngx/CLAUDE.md b/ui-ngx/CLAUDE.md index 0ce231c91c..23d85b5f56 100644 --- a/ui-ngx/CLAUDE.md +++ b/ui-ngx/CLAUDE.md @@ -15,6 +15,7 @@ | Alias | Maps to | |-------|---------| | `@app/*` | `src/app/*` | +| `@env/*` | `src/environments/*` | | `@core/*` | `src/app/core/*` | | `@modules/*` | `src/app/modules/*` | | `@home/*` | `src/app/modules/home/*` | @@ -167,7 +168,7 @@ Source paths below are relative to `src/app/modules/` (not `home/pages/`). | `/resources/widgets-library/widget-types/:widgetTypeId` | `widget/` | custom (WidgetEditorComponent) | — | | `/resources/images` | `admin/` | custom (ImageGalleryComponent) | — | | `/resources/scada-symbols` | `admin/` | custom (ImageGalleryComponent) | — | -| `/resources/scada-symbols/:type/:key` | `admin/` | custom (ScadaSymbolComponent) | — | +| `/resources/scada-symbols/:type/:key` | `scada-symbol/` | custom (ScadaSymbolComponent) | — | | `/resources/resources-library` | `admin/` | entity-table | `ResourcesLibraryTableConfigResolver` | | `/resources/resources-library/:entityId` | `admin/` | entity-details | `ResourcesLibraryTableConfigResolver` | | `/resources/javascript-library` | `admin/` | entity-table | `JsLibraryTableConfigResolver` | @@ -277,15 +278,15 @@ Most pages follow a 3-file pattern per entity type inside `pages//`: | `EntityViewsTableConfigResolver` | ENTITY_VIEW | `entity-view/` | `EntityViewComponent` | `EntityViewTabsComponent` | | `CustomersTableConfigResolver` | CUSTOMER | `customer/` | `CustomerComponent` | `CustomerTabsComponent` | | `UsersTableConfigResolver` | USER | `user/` | `UserComponent` | `UserTabsComponent` | -| `DashboardsTableConfigResolver` | DASHBOARD | `dashboard/` | — | — | +| `DashboardsTableConfigResolver` | DASHBOARD | `dashboard/` | `DashboardFormComponent` | `DashboardTabsComponent` | | `EdgesTableConfigResolver` | EDGE | `edge/` | `EdgeComponent` | `EdgeTabsComponent` | | `RuleChainsTableConfigResolver` | RULE_CHAIN | `rulechain/` | `RuleChainComponent` | `RuleChainTabsComponent` | -| `OtaUpdateTableConfigResolve` | OTA_PACKAGE | `ota-update/` | — | — | -| `CalculatedFieldsTableConfigResolver` | CALCULATED_FIELD | `calculated-fields/` | — | — | +| `OtaUpdateTableConfigResolve` | OTA_PACKAGE | `ota-update/` | `OtaUpdateComponent` | `OtaUpdateTabsComponent` | +| `CalculatedFieldsTableConfigResolver` | CALCULATED_FIELD | `calculated-fields/` (config in `home/components/calculated-fields/`) | `CalculatedFieldComponent` | `CalculatedFieldsTabsComponent` | | `WidgetTypesTableConfigResolver` | WIDGETS_BUNDLE | `widget/` | `WidgetTypeComponent` | `WidgetTypeTabsComponent` | | `WidgetsBundlesTableConfigResolver` | WIDGETS_BUNDLE | `widget/` | `WidgetsBundleComponent` | `WidgetsBundleTabsComponent` | -| `ResourcesLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | — | — | -| `JsLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | — | — | +| `ResourcesLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | `ResourcesLibraryComponent` | `ResourceLibraryTabsComponent` | +| `JsLibraryTableConfigResolver` | TB_RESOURCE | `admin/` | `JsResourceComponent` | `ResourceTabsComponent` | | `QueuesTableConfigResolver` | QUEUE | `admin/queue/` | `QueueComponent` | — | | `AiModelsTableConfigResolver` | AI_MODEL | `ai-model/` | — | — | | `ClientsTableConfigResolver` | OAUTH2_CLIENT | `admin/oauth2/clients/` | `ClientComponent` | — | @@ -740,3 +741,6 @@ All services are in `src/app/core/http/`. | Trendz Settings | `trendz-settings.service.ts` | `TrendzSettingsService` | | Usage Info | `usage-info.service.ts` | `UsageInfoService` | | Component Descriptor | `component-descriptor.service.ts` | `ComponentDescriptorService` | +| GitHub | `git-hub.service.ts` | `GitHubService` | +| Mobile Application | `mobile-application.service.ts` | `MobileApplicationService` | +| UI Settings | `ui-settings.service.ts` | `UiSettingsService` | From 932d8c64355c9318f05919e7c5e7cc204bcb79c7 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 15:29:48 +0200 Subject: [PATCH 175/313] Rename ui-ngx help doc to structure.md --- ui-ngx/{CLAUDE.md => structure.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ui-ngx/{CLAUDE.md => structure.md} (100%) diff --git a/ui-ngx/CLAUDE.md b/ui-ngx/structure.md similarity index 100% rename from ui-ngx/CLAUDE.md rename to ui-ngx/structure.md From 8246097fb45553550a974468c7c3191b02379796 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 16:32:42 +0200 Subject: [PATCH 176/313] Create unified ComplexOperation enum and update all references - Add top-level ComplexOperation enum in query package - Delete duplicate alarm.rule.condition.expression.ComplexOperation - Remove nested ComplexFilterPredicate.ComplexOperation enum - Update imports in 8 files across common/data, common/edqs, edqs, and application --- .../cf/ctx/state/alarm/AlarmCalculatedFieldState.java | 2 +- .../service/install/DefaultSystemDataLoaderService.java | 2 +- .../test/java/org/thingsboard/server/cf/AlarmRulesTest.java | 2 +- .../rule/condition/expression/AlarmConditionFilter.java | 1 + .../condition/expression/SimpleAlarmConditionExpression.java | 1 + .../expression/predicate/ComplexFilterPredicate.java | 2 +- .../server/common/data/query/ComplexFilterPredicate.java | 5 ----- .../condition/expression => query}/ComplexOperation.java | 2 +- .../org/thingsboard/server/edqs/util/RepositoryUtils.java | 4 ++-- .../thingsboard/server/edqs/repo/RepositoryUtilsTest.java | 2 +- 10 files changed, 10 insertions(+), 13 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/{alarm/rule/condition/expression => query}/ComplexOperation.java (89%) 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 1719c95f7a..930d030d71 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 @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionTyp import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; -import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 965d7105a9..d528f10899 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -53,7 +53,7 @@ import org.thingsboard.server.common.data.alarm.rule.AlarmRule; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; -import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index cf94940e07..5824b832df 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -38,7 +38,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.RepeatingAlarmCon import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; -import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java index 75849967a0..d80eccb9fc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java @@ -24,6 +24,7 @@ import lombok.Data; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityKeyValueType; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java index 390bbb3230..a581fa3a41 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java @@ -22,6 +22,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType; +import org.thingsboard.server.common.data.query.ComplexOperation; import java.util.List; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java index 17c5279b3e..34f128d3e2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java @@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predi import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.query.ComplexOperation; import java.util.List; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java index 557f08eb2b..248211ad92 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java @@ -34,9 +34,4 @@ public class ComplexFilterPredicate implements KeyFilterPredicate { return FilterPredicateType.COMPLEX; } - @Schema - public enum ComplexOperation { - AND, - OR - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexOperation.java similarity index 89% rename from common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java rename to common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexOperation.java index 492fc683dd..4f0db45faa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexOperation.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.alarm.rule.condition.expression; +package org.thingsboard.server.common.data.query; public enum ComplexOperation { AND, diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java index a0a030ba08..31fc82225e 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java @@ -60,8 +60,8 @@ import java.util.stream.Stream; import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; import static org.thingsboard.server.common.data.StringUtils.equalsAny; import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; -import static org.thingsboard.server.common.data.query.ComplexFilterPredicate.ComplexOperation.AND; -import static org.thingsboard.server.common.data.query.ComplexFilterPredicate.ComplexOperation.OR; +import static org.thingsboard.server.common.data.query.ComplexOperation.AND; +import static org.thingsboard.server.common.data.query.ComplexOperation.OR; @Slf4j public class RepositoryUtils { diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java index a3691a0889..557cfb7a4a 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.BooleanFilterPredicate.BooleanOperation; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; -import org.thingsboard.server.common.data.query.ComplexFilterPredicate.ComplexOperation; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; From c9e0839b5294a4c9f0e67eec6e6a775bb96ca109 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 16:34:28 +0200 Subject: [PATCH 177/313] Add keyFiltersOperation field to EntityCountQuery and propagate through constructors - Add keyFiltersOperation field with null-safe getKeyFiltersOperationOrDefault() - Add 3-arg constructor to EntityCountQuery for explicit operation - Add 6-arg constructors to AbstractDataQuery and EntityDataQuery - Add 7-arg constructor to AlarmDataQuery - Update next() methods to propagate keyFiltersOperation - Update buildEntityDataQuery methods to pass operation from alarm queries --- .../service/query/DefaultEntityQueryService.java | 4 ++-- .../server/common/data/query/AbstractDataQuery.java | 12 ++++++++++++ .../server/common/data/query/AlarmDataQuery.java | 7 ++++++- .../server/common/data/query/EntityCountQuery.java | 13 +++++++++++++ .../server/common/data/query/EntityDataQuery.java | 6 +++++- 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 50f0f0e75a..92d76693f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -221,7 +221,7 @@ public class DefaultEntityQueryService implements EntityQueryService { private EntityDataQuery buildEntityDataQuery(AlarmCountQuery query) { EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, EntityKeyMapping.CREATED_TIME))); - return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } private EntityDataQuery buildEntityDataQuery(AlarmDataQuery query) { @@ -233,7 +233,7 @@ public class DefaultEntityQueryService implements EntityQueryService { entitiesSortOrder = sortOrder; } EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); - return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java index a678338864..3aabbae0a8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java @@ -49,4 +49,16 @@ public abstract class AbstractDataQuery extends En this.latestValues = latestValues; } + public AbstractDataQuery(EntityFilter entityFilter, + T pageLink, + List entityFields, + List latestValues, + List keyFilters, + ComplexOperation keyFiltersOperation) { + super(entityFilter, keyFilters, keyFiltersOperation); + this.pageLink = pageLink; + this.entityFields = entityFields; + this.latestValues = latestValues; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java index 8d2ae6fae3..072f56abd1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java @@ -39,8 +39,13 @@ public class AlarmDataQuery extends AbstractDataQuery { this.alarmFields = alarmFields; } + public AlarmDataQuery(EntityFilter entityFilter, AlarmDataPageLink pageLink, List entityFields, List latestValues, List keyFilters, List alarmFields, ComplexOperation keyFiltersOperation) { + super(entityFilter, pageLink, entityFields, latestValues, keyFilters, keyFiltersOperation); + this.alarmFields = alarmFields; + } + @JsonIgnore public AlarmDataQuery next() { - return new AlarmDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters, alarmFields); + return new AlarmDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters, alarmFields, getKeyFiltersOperation()); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java index b4680bfd5b..ee39fe7d77 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java @@ -34,6 +34,9 @@ public class EntityCountQuery { @Getter protected List keyFilters; + @Getter + private ComplexOperation keyFiltersOperation; + public EntityCountQuery() { } @@ -45,4 +48,14 @@ public class EntityCountQuery { this.entityFilter = entityFilter; this.keyFilters = keyFilters; } + + public EntityCountQuery(EntityFilter entityFilter, List keyFilters, ComplexOperation keyFiltersOperation) { + this.entityFilter = entityFilter; + this.keyFilters = keyFilters; + this.keyFiltersOperation = keyFiltersOperation; + } + + public ComplexOperation getKeyFiltersOperationOrDefault() { + return keyFiltersOperation != null ? keyFiltersOperation : ComplexOperation.AND; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java index 959e3e3af6..f24ffd5821 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java @@ -34,9 +34,13 @@ public class EntityDataQuery extends AbstractDataQuery { super(entityFilter, pageLink, entityFields, latestValues, keyFilters); } + public EntityDataQuery(EntityFilter entityFilter, EntityDataPageLink pageLink, List entityFields, List latestValues, List keyFilters, ComplexOperation keyFiltersOperation) { + super(entityFilter, pageLink, entityFields, latestValues, keyFilters, keyFiltersOperation); + } + @JsonIgnore public EntityDataQuery next() { - return new EntityDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters); + return new EntityDataQuery(getEntityFilter(), getPageLink().nextPageLink(), entityFields, latestValues, keyFilters, getKeyFiltersOperation()); } } From 4ae486a5f1173784aeb7f66f339b1aba9a217763 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 16:38:29 +0200 Subject: [PATCH 178/313] Add feature toggle, SystemParams field, and service-layer validation - Add sql.query.key-filters-or-conditions.enabled property to thingsboard.yml - Add allowKeyFiltersOrConditions field to SystemParams - Wire config value into SystemInfoController for /api/system/params - Add validateKeyFiltersOperation to DefaultEntityQueryService - Add throws ThingsboardException to EntityQueryService interface methods - Reject OR queries with 400 when feature is disabled --- .../controller/SystemInfoController.java | 4 ++++ .../query/DefaultEntityQueryService.java | 22 +++++++++++++++---- .../service/query/EntityQueryService.java | 9 ++++---- .../src/main/resources/thingsboard.yml | 4 ++++ .../server/common/data/SystemParams.java | 1 + 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index ee2e871d6e..9bcd756de0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -76,6 +76,9 @@ public class SystemInfoController extends BaseController { @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; + @Value("${sql.query.key-filters-or-conditions.enabled:true}") + private boolean keyFiltersOrConditionsEnabled; + @Autowired(required = false) private BuildProperties buildProperties; @@ -173,6 +176,7 @@ public class SystemInfoController extends BaseController { systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) .map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) .orElse(false)); + systemParams.setAllowKeyFiltersOrConditions(keyFiltersOrConditionsEnabled); return systemParams; } diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 92d76693f9..00426dae18 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeySample; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; @@ -93,6 +94,9 @@ public class DefaultEntityQueryService implements EntityQueryService { @Value("${server.ws.max_entities_per_alarm_subscription:1000}") private int maxEntitiesPerAlarmSubscription; + @Value("${sql.query.key-filters-or-conditions.enabled:true}") + private boolean keyFiltersOrConditionsEnabled; + @Autowired private DbCallbackExecutorService dbCallbackExecutor; @@ -103,12 +107,14 @@ public class DefaultEntityQueryService implements EntityQueryService { private AttributesService attributesService; @Override - public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) { + public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) throws ThingsboardException { + validateKeyFiltersOperation(query); return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); } @Override - public PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) { + public PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) throws ThingsboardException { + validateKeyFiltersOperation(query); if (query.getKeyFilters() != null) { resolveDynamicValuesInPredicates( query.getKeyFilters().stream() @@ -177,7 +183,8 @@ public class DefaultEntityQueryService implements EntityQueryService { } @Override - public PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) { + public PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) throws ThingsboardException { + validateKeyFiltersOperation(query); EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); PageData entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), entityDataQuery); @@ -203,7 +210,8 @@ public class DefaultEntityQueryService implements EntityQueryService { } @Override - public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) { + public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) throws ThingsboardException { + validateKeyFiltersOperation(query); if (query.getEntityFilter() != null) { EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); PageData entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), @@ -236,6 +244,12 @@ public class DefaultEntityQueryService implements EntityQueryService { return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } + private void validateKeyFiltersOperation(EntityCountQuery query) throws ThingsboardException { + if (!keyFiltersOrConditionsEnabled && query.getKeyFiltersOperation() == ComplexOperation.OR) { + throw new ThingsboardException("OR conditions between key filters are disabled", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + @Override public ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, boolean isTimeseries, boolean isAttributes, AttributeScope scope) { diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java index 354f8e9278..b3d2f208ca 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.query; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; @@ -33,13 +34,13 @@ import java.util.Set; public interface EntityQueryService { - long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query); + long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) throws ThingsboardException; - PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query); + PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) throws ThingsboardException; - PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query); + PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) throws ThingsboardException; - long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query); + long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) throws ThingsboardException; ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, boolean isTimeseries, boolean isAttributes, AttributeScope scope); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 15d6afb37d..f21cd5c7dd 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -469,6 +469,10 @@ sql: log_tenant_stats: "${SQL_LOG_TENANT_STATS:true}" # Interval in milliseconds for printing the latest statistic information about the tenant log_tenant_stats_interval_ms: "${SQL_LOG_TENANT_STATS_INTERVAL_MS:60000}" + query: + key-filters-or-conditions: + # Enable/disable OR conditions between key filters in entity data queries + enabled: "${SQL_QUERY_KEY_FILTERS_OR_CONDITIONS_ENABLED:true}" postgres: # Specify partitioning size for timestamp key-value storage. Example: DAYS, MONTHS, YEARS, INDEFINITE. ts_key_value_partitioning: "${SQL_POSTGRES_TS_KV_PARTITIONING:MONTHS}" diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index 3cd2ada38f..d3b2e8b73a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -45,4 +45,5 @@ public class SystemParams { long minAllowedAggregationIntervalInSecForCF; long intermediateAggregationIntervalInSecForCF; TrendzSettings trendzSettings; + boolean allowKeyFiltersOrConditions; } From ce68c8fca512647be3a03f078a481a9f4678f48e Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 16:43:30 +0200 Subject: [PATCH 179/313] Add ComplexOperation field to EDQS query model and propagate through toNewQuery - Add keyFiltersOperation field to EdqsQuery base class - Update EdqsCountQuery and EdqsDataQuery constructors to accept and propagate ComplexOperation - Propagate keyFiltersOperationOrDefault in both toNewQuery overloads in RepositoryUtils --- .../org/thingsboard/server/edqs/query/EdqsCountQuery.java | 5 +++-- .../org/thingsboard/server/edqs/query/EdqsDataQuery.java | 5 +++-- .../java/org/thingsboard/server/edqs/query/EdqsQuery.java | 2 ++ .../org/thingsboard/server/edqs/util/RepositoryUtils.java | 2 ++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java index 0d2ed67987..d338362aeb 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsCountQuery.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.query; import lombok.Builder; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityFilter; import java.util.List; @@ -23,8 +24,8 @@ import java.util.List; public class EdqsCountQuery extends EdqsQuery { @Builder - EdqsCountQuery(EntityFilter entityFilter, boolean hasKeyFilters, List keyFilters) { - super(entityFilter, hasKeyFilters, keyFilters); + EdqsCountQuery(EntityFilter entityFilter, boolean hasKeyFilters, List keyFilters, ComplexOperation keyFiltersOperation) { + super(entityFilter, hasKeyFilters, keyFilters, keyFiltersOperation); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java index 72ccb68515..bd93eca2f1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsDataQuery.java @@ -19,6 +19,7 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.data.query.EntityKeyType; @@ -41,10 +42,10 @@ public class EdqsDataQuery extends EdqsQuery { private final List latestValues; @Builder - public EdqsDataQuery(EntityFilter entityFilter, List keyFilters, + public EdqsDataQuery(EntityFilter entityFilter, List keyFilters, ComplexOperation keyFiltersOperation, int pageSize, int page, String textSearch, DataKey sortKey, EntityDataSortOrder.Direction sortDirection, List entityFields, List latestValues) { - super(entityFilter, CollectionsUtil.isNotEmpty(keyFilters), keyFilters); + super(entityFilter, CollectionsUtil.isNotEmpty(keyFilters), keyFilters, keyFiltersOperation); this.pageSize = pageSize; this.page = page; this.hasTextSearch = StringUtils.isNotBlank(textSearch); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java index 9cfc5d442c..8d29ec89b8 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/EdqsQuery.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.query; import lombok.Data; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityFilter; import java.util.List; @@ -26,5 +27,6 @@ public abstract class EdqsQuery { private final EntityFilter entityFilter; private final boolean hasKeyFilters; private final List keyFilters; + private final ComplexOperation keyFiltersOperation; } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java index 31fc82225e..9aaa6a5927 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java @@ -139,6 +139,7 @@ public class RepositoryUtils { } query.entityFilter(oldQuery.getEntityFilter()); query.keyFilters(toKeyFilters(oldQuery.getKeyFilters())); + query.keyFiltersOperation(oldQuery.getKeyFiltersOperationOrDefault()); query.entityFields(toNewKeys(oldQuery.getEntityFields())); query.latestValues(toNewKeys(oldQuery.getLatestValues())); return query.build(); @@ -149,6 +150,7 @@ public class RepositoryUtils { .entityFilter(oldQuery.getEntityFilter()) .hasKeyFilters(CollectionsUtil.isNotEmpty(oldQuery.getKeyFilters())) .keyFilters(toKeyFilters(oldQuery.getKeyFilters())) + .keyFiltersOperation(oldQuery.getKeyFiltersOperationOrDefault()) .build(); } From caac757f181d59235c91a7f808dcd2b88faf4831 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 16:44:52 +0200 Subject: [PATCH 180/313] Implement OR logic in EDQS checkKeyFilters with DRY helper extraction - Extract evaluateSingleFilter helper to avoid duplicating filter evaluation logic - Add 3-arg checkKeyFilters overload accepting ComplexOperation parameter - OR mode: any matching filter includes entity (short-circuit on first true) - AND mode: all filters must match (short-circuit on first false, unchanged behavior) - Null operation defaults to AND for backward compatibility - Update checkFilters to pass query.getKeyFiltersOperation() through to checkKeyFilters --- .../server/edqs/util/RepositoryUtils.java | 76 ++++++++++++------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java index 9aaa6a5927..f6da57e24c 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; @@ -198,38 +199,55 @@ public class RepositoryUtils { } public static boolean checkKeyFilters(EntityData entity, List keyFilters) { - for (EdqsFilter keyFilter : keyFilters) { - EntityKeyValueType valueType = keyFilter.valueType(); - if (valueType == null) { - valueType = switch (keyFilter.predicate().getType()) { - case STRING -> EntityKeyValueType.STRING; - case NUMERIC -> EntityKeyValueType.NUMERIC; - case BOOLEAN -> EntityKeyValueType.BOOLEAN; - default -> throw new IllegalStateException(); - }; - } - DataKey dataKey = keyFilter.key(); - DataPoint dp = entity.getDataPoint(dataKey, null); - boolean checkResult = switch (valueType) { - case STRING -> { - String str = dp != null ? dp.valueToString() : null; - yield (dataKey.type() == EntityKeyType.ENTITY_FIELD) ? (str == null || checkKeyFilter(str, keyFilter.predicate())) : - (str != null && checkKeyFilter(str, keyFilter.predicate())); - } - case BOOLEAN -> { - Boolean booleanValue = dp != null ? dp.getBool() : null; - yield booleanValue != null && checkKeyFilter(booleanValue, keyFilter.predicate()); + return checkKeyFilters(entity, keyFilters, ComplexOperation.AND); + } + + public static boolean checkKeyFilters(EntityData entity, List keyFilters, ComplexOperation operation) { + ComplexOperation op = operation != null ? operation : ComplexOperation.AND; + if (op == ComplexOperation.OR) { + for (EdqsFilter keyFilter : keyFilters) { + if (evaluateSingleFilter(entity, keyFilter)) { + return true; } - case DATE_TIME, NUMERIC -> { - Double doubleValue = dp != null ? dp.getDouble() : null; - yield doubleValue != null && checkKeyFilter(doubleValue, keyFilter.predicate()); + } + return keyFilters.isEmpty(); + } else { + for (EdqsFilter keyFilter : keyFilters) { + if (!evaluateSingleFilter(entity, keyFilter)) { + return false; } - }; - if (!checkResult) { - return false; } + return true; } - return true; + } + + private static boolean evaluateSingleFilter(EntityData entity, EdqsFilter keyFilter) { + EntityKeyValueType valueType = keyFilter.valueType(); + if (valueType == null) { + valueType = switch (keyFilter.predicate().getType()) { + case STRING -> EntityKeyValueType.STRING; + case NUMERIC -> EntityKeyValueType.NUMERIC; + case BOOLEAN -> EntityKeyValueType.BOOLEAN; + default -> throw new IllegalStateException(); + }; + } + DataKey dataKey = keyFilter.key(); + DataPoint dp = entity.getDataPoint(dataKey, null); + return switch (valueType) { + case STRING -> { + String str = dp != null ? dp.valueToString() : null; + yield (dataKey.type() == EntityKeyType.ENTITY_FIELD) ? (str == null || checkKeyFilter(str, keyFilter.predicate())) : + (str != null && checkKeyFilter(str, keyFilter.predicate())); + } + case BOOLEAN -> { + Boolean booleanValue = dp != null ? dp.getBool() : null; + yield booleanValue != null && checkKeyFilter(booleanValue, keyFilter.predicate()); + } + case DATE_TIME, NUMERIC -> { + Double doubleValue = dp != null ? dp.getDouble() : null; + yield doubleValue != null && checkKeyFilter(doubleValue, keyFilter.predicate()); + } + }; } public static boolean checkKeyFilter(String value, KeyFilterPredicate keyFilterPredicate) { @@ -378,7 +396,7 @@ public class RepositoryUtils { if (entity == null || entity.getFields() == null) { return false; // Entity was already removed or not arrived yet; } - if (query.isHasKeyFilters() && !checkKeyFilters(entity, query.getKeyFilters())) { + if (query.isHasKeyFilters() && !checkKeyFilters(entity, query.getKeyFilters(), query.getKeyFiltersOperation())) { return false; } if (query instanceof EdqsDataQuery dataQuery) { From daa092ebe977125c5a7c5fec0f818e0de185cf74 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 16:45:44 +0200 Subject: [PATCH 181/313] OR-aware SQL generation in EntityKeyMapping - Add 4-arg buildQuery overload with ComplexOperation parameter that joins predicates with OR - Add 3-arg toQueries overload with outerContext flag for bare column names in outer WHERE - Add forceLeftJoin parameter to toLatestJoin and buildLatestJoins for LEFT JOIN under OR - Modify prepareKeyMapping to create individual mappings per KeyFilter under OR (no same-key grouping) - Modify prepareEntityCountKeyMapping with same OR ungrouping logic - All existing 2-arg/3-arg overloads preserved as backward-compatible delegates --- .../dao/sql/query/EntityKeyMapping.java | 130 +++++++++++++++--- 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index b9a134e713..72096dbf6e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; @@ -283,8 +284,33 @@ public class EntityKeyMapping { } public Stream toQueries(SqlQueryContext ctx, EntityFilterType filterType) { + return toQueries(ctx, filterType, false); + } + + public Stream toQueries(SqlQueryContext ctx, EntityFilterType filterType, boolean outerContext) { if (hasFilter()) { - String keyAlias = (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) ? "e" : alias; + String keyAlias; + if (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) { + if (outerContext) { + // In the outer query (OR relocation), entity field columns are available + // as bare column names from the inner subquery SELECT. + // buildSimplePredicateQuery does: alias + "." + entityKeyColumn when entityKeyColumn != null. + // To produce just the bare column name, temporarily null out entityKeyColumn + // so buildSimplePredicateQuery uses alias directly as the field. + String bareColumn = getEntityKeyColumn(); + String savedColumn = this.entityKeyColumn; + this.entityKeyColumn = null; + List predicates = keyFilters.stream() + .map(keyFilter -> this.buildKeyQuery(ctx, bareColumn, keyFilter, filterType)) + .collect(Collectors.toList()); + this.entityKeyColumn = savedColumn; + return predicates.stream(); + } else { + keyAlias = "e"; + } + } else { + keyAlias = alias; + } return keyFilters.stream().map(keyFilter -> this.buildKeyQuery(ctx, keyAlias, keyFilter, filterType)); } else { @@ -293,6 +319,10 @@ public class EntityKeyMapping { } public String toLatestJoin(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType) { + return toLatestJoin(ctx, entityFilter, entityType, false); + } + + public String toLatestJoin(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType, boolean forceLeftJoin) { String entityTypeStr; if (entityFilter.getType().equals(EntityFilterType.RELATIONS_QUERY)) { entityTypeStr = "entities.entity_type"; @@ -307,13 +337,13 @@ public class EntityKeyMapping { filterQuery = " AND (" + filterQuery + ")"; } if (entityKey.getType().equals(EntityKeyType.TIME_SERIES)) { - String join = (hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join"; + String join = (!forceLeftJoin && hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join"; return String.format("%s ts_kv_latest %s ON %s.entity_id=entities.id AND %s.key = (select key_id from key_dictionary where key = :%s_key_id) %s", join, alias, alias, alias, alias, filterQuery); } else { String query; if (!entityKey.getType().equals(EntityKeyType.ATTRIBUTE)) { - String join = (hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join"; + String join = (!forceLeftJoin && hasFilter() && hasFilterValues(ctx)) ? "inner join" : "left join"; query = String.format("%s attribute_kv %s ON %s.entity_id=entities.id AND %s.attribute_key=(select key_id from key_dictionary where key = :%s_key_id) ", join, alias, alias, alias, alias); int scope; @@ -326,7 +356,7 @@ public class EntityKeyMapping { } query = String.format("%s AND %s.attribute_type=%s %s", query, alias, scope, filterQuery); } else { - String join = (hasFilter() && hasFilterValues(ctx)) ? "join LATERAL" : "left join LATERAL"; + String join = (!forceLeftJoin && hasFilter() && hasFilterValues(ctx)) ? "join LATERAL" : "left join LATERAL"; query = String.format("%s (select * from attribute_kv %s WHERE %s.entity_id=entities.id AND %s.attribute_key=(select key_id from key_dictionary where key = :%s_key_id) %s " + "ORDER BY %s.last_update_ts DESC limit 1) as %s ON true", join, alias, alias, alias, alias, filterQuery, alias, alias); @@ -351,27 +381,44 @@ public class EntityKeyMapping { } public static String buildLatestJoins(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType, List latestMappings, boolean countQuery) { + return buildLatestJoins(ctx, entityFilter, entityType, latestMappings, countQuery, false); + } + + public static String buildLatestJoins(SqlQueryContext ctx, EntityFilter entityFilter, EntityType entityType, + List latestMappings, boolean countQuery, boolean forceLeftJoin) { return latestMappings.stream() .filter(mapping -> !countQuery || mapping.hasFilter()) - .map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType)) + .map(mapping -> mapping.toLatestJoin(ctx, entityFilter, entityType, forceLeftJoin)) .collect(Collectors.joining(" ")); } public static String buildQuery(SqlQueryContext ctx, List mappings, EntityFilterType filterType) { + return buildQuery(ctx, mappings, filterType, ComplexOperation.AND); + } + + public static String buildQuery(SqlQueryContext ctx, List mappings, + EntityFilterType filterType, ComplexOperation operation) { + String joiner = (operation == ComplexOperation.OR) ? " OR " : " AND "; return mappings.stream() - .flatMap(mapping -> mapping.toQueries(ctx, filterType)) + .flatMap(mapping -> mapping.toQueries(ctx, filterType, operation == ComplexOperation.OR)) .filter(StringUtils::isNotEmpty) - .collect(Collectors.joining(" AND ")); + .collect(Collectors.joining(joiner)); } public static List prepareKeyMapping(EntityType entityType, EntityDataQuery query) { EntityFilterType entityFilterType = query.getEntityFilter().getType(); + ComplexOperation operation = query.getKeyFiltersOperationOrDefault(); List entityFields = query.getEntityFields() != null ? query.getEntityFields() : Collections.emptyList(); List latestValues = query.getLatestValues() != null ? query.getLatestValues() : Collections.emptyList(); - Map> filters = - query.getKeyFilters() != null ? - query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); + Map> filters; + if (operation == ComplexOperation.OR) { + // Under OR, don't group same-key filters; handle individually below + filters = Collections.emptyMap(); + } else { + filters = query.getKeyFilters() != null ? + query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); + } EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityKey sortOrderKey = sortOrder != null ? sortOrder.getKey() : null; int index = 2; @@ -428,14 +475,37 @@ public class EntityKeyMapping { for (EntityKeyMapping mapping : mappings) { mapping.setIndex(index); mapping.setAlias(String.format("alias%s", index)); - mapping.setKeyFilters(filters.remove(mapping.entityKey)); + if (operation != ComplexOperation.OR) { + // Under AND: assign grouped filters to matching selection mappings + mapping.setKeyFilters(filters.remove(mapping.entityKey)); + } if (mapping.getEntityKey().getType().equals(EntityKeyType.ENTITY_FIELD)) { index++; } else { index += 2; } } - if (!filters.isEmpty()) { + if (operation == ComplexOperation.OR && query.getKeyFilters() != null) { + // Under OR: each KeyFilter gets its own EntityKeyMapping (no same-key grouping) + for (KeyFilter keyFilter : query.getKeyFilters()) { + EntityKeyMapping mapping = new EntityKeyMapping(); + mapping.setIndex(index); + mapping.setAlias(String.format("alias%s", index)); + mapping.setKeyFilters(Collections.singletonList(keyFilter)); + EntityKey filterKey = keyFilter.getKey(); + mapping.setLatest(!filterKey.getType().equals(EntityKeyType.ENTITY_FIELD)); + mapping.setEntityKey(filterKey); + mapping.setEntityKeyColumn(entityType, entityFilterType); + mapping.setSelection(mapping.getEntityKeyColumn() == null); + mappings.add(mapping); + if (filterKey.getType().equals(EntityKeyType.ENTITY_FIELD)) { + index += 1; + } else { + index += 2; + } + } + } else if (!filters.isEmpty()) { + // Under AND: create mappings for filter-only keys not in selection for (EntityKey filterField : filters.keySet()) { EntityKeyMapping mapping = new EntityKeyMapping(); mapping.setIndex(index); @@ -464,25 +534,45 @@ public class EntityKeyMapping { public static List prepareEntityCountKeyMapping(EntityCountQuery query) { EntityType entityType = resolveEntityType(query.getEntityFilter()); EntityFilterType entityFilterType = query.getEntityFilter().getType(); + ComplexOperation operation = query.getKeyFiltersOperationOrDefault(); - Map> filters = - query.getKeyFilters() != null ? - query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); int index = 2; List mappings = new ArrayList<>(); - if (!filters.isEmpty()) { - for (EntityKey filterField : filters.keySet()) { + + if (operation == ComplexOperation.OR && query.getKeyFilters() != null && !query.getKeyFilters().isEmpty()) { + // Under OR: each KeyFilter gets its own mapping (no same-key grouping) + for (KeyFilter keyFilter : query.getKeyFilters()) { EntityKeyMapping mapping = new EntityKeyMapping(); mapping.setIndex(index); mapping.setAlias(String.format("alias%s", index)); - mapping.setKeyFilters(filters.get(filterField)); - mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD)); - mapping.setEntityKey(filterField); + mapping.setKeyFilters(Collections.singletonList(keyFilter)); + EntityKey filterKey = keyFilter.getKey(); + mapping.setLatest(!filterKey.getType().equals(EntityKeyType.ENTITY_FIELD)); + mapping.setEntityKey(filterKey); mapping.setEntityKeyColumn(entityType, entityFilterType); mapping.setSelection(mapping.getEntityKeyColumn() == null); mappings.add(mapping); index += 1; } + } else { + // Under AND (or null/default): group same-key filters together + Map> filters = + query.getKeyFilters() != null ? + query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); + if (!filters.isEmpty()) { + for (EntityKey filterField : filters.keySet()) { + EntityKeyMapping mapping = new EntityKeyMapping(); + mapping.setIndex(index); + mapping.setAlias(String.format("alias%s", index)); + mapping.setKeyFilters(filters.get(filterField)); + mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD)); + mapping.setEntityKey(filterField); + mapping.setEntityKeyColumn(entityType, entityFilterType); + mapping.setSelection(mapping.getEntityKeyColumn() == null); + mappings.add(mapping); + index += 1; + } + } } return mappings; From 51bb37aa962f33a7c02e91ec139e04bd84b52f64 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 16:47:50 +0200 Subject: [PATCH 182/313] Thread OR operation through DefaultEntityQueryRepository query building - Read keyFiltersOperationOrDefault in countEntitiesByQuery and findEntityDataByQuery - Under OR: relocate entity field filter predicates from inner WHERE to outer WHERE - Under OR: pass forceLeftJoin=true to buildLatestJoins for all aliases - Under OR: combine all filter mappings with OR joiner via buildQuery(ComplexOperation.OR) - Under OR: ensure entity field filter columns are in inner SELECT for outer WHERE reference - AND path remains completely unchanged (same execution path as before) --- .../query/DefaultEntityQueryRepository.java | 87 +++++++++++++++---- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index a9b04618c8..94e615a1bb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.ApiUsageStateFilter; import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.AssetTypeFilter; import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter; @@ -54,6 +55,7 @@ import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -351,25 +353,48 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } }); } else { - List mappings = EntityKeyMapping.prepareEntityCountKeyMapping(query); + ComplexOperation operation = query.getKeyFiltersOperationOrDefault(); + boolean isOr = operation == ComplexOperation.OR; - List selectionMapping = mappings.stream().filter(EntityKeyMapping::isSelection) - .collect(Collectors.toList()); - List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) - .collect(Collectors.toList()); + List mappings = EntityKeyMapping.prepareEntityCountKeyMapping(query); + List selectionMapping = new ArrayList<>(mappings.stream().filter(EntityKeyMapping::isSelection) + .collect(Collectors.toList())); List filterMapping = mappings.stream().filter(EntityKeyMapping::hasFilter) .collect(Collectors.toList()); List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest() && mapping.getEntityKeyColumn() != null) .collect(Collectors.toList()); + // Under OR: entity field filter columns must be in inner SELECT for outer WHERE reference + if (isOr) { + for (EntityKeyMapping m : entityFieldsFiltersMapping) { + if (!selectionMapping.contains(m)) { + selectionMapping.add(m); + } + } + } + + List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + List allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest) .collect(Collectors.toList()); + // Under OR: entity field filters move to outer WHERE (not inner WHERE) + List innerEntityFieldsFilters = isOr ? Collections.emptyList() : entityFieldsFiltersMapping; + + String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), innerEntityFieldsFilters); + String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true, isOr); + + // Under OR: combine ALL filter mappings (entity fields + aliases) in outer WHERE with OR joiner + String aliasWhereQuery; + if (isOr) { + String combinedFilterQuery = EntityKeyMapping.buildQuery(ctx, filterMapping, query.getEntityFilter().getType(), ComplexOperation.OR); + aliasWhereQuery = combinedFilterQuery.isEmpty() ? "" : " where (" + combinedFilterQuery + ")"; + } else { + aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, ""); + } - String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping); - String aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, ""); - String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true); String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); String entityTypeStr; if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { @@ -419,13 +444,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { EntityType entityType = resolveEntityType(query.getEntityFilter()); SqlQueryContext ctx = new SqlQueryContext(new QueryContext(tenantId, customerId, entityType, ignorePermissionCheck)); EntityDataPageLink pageLink = query.getPageLink(); + ComplexOperation operation = query.getKeyFiltersOperationOrDefault(); + boolean isOr = operation == ComplexOperation.OR; List mappings = EntityKeyMapping.prepareKeyMapping(entityType, query); - List selectionMapping = mappings.stream().filter(EntityKeyMapping::isSelection) - .collect(Collectors.toList()); - List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) - .collect(Collectors.toList()); + List selectionMapping = new ArrayList<>(mappings.stream().filter(EntityKeyMapping::isSelection) + .collect(Collectors.toList())); List latestSelectionMapping = selectionMapping.stream().filter(EntityKeyMapping::isLatest) .collect(Collectors.toList()); @@ -434,14 +459,44 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest() && mapping.getEntityKeyColumn() != null) .collect(Collectors.toList()); + // Under OR: entity field filter columns must be in inner SELECT for outer WHERE reference + if (isOr) { + for (EntityKeyMapping m : entityFieldsFiltersMapping) { + if (!selectionMapping.contains(m)) { + selectionMapping.add(m); + } + } + } + + List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + List allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest) .collect(Collectors.toList()); + // Under OR: entity field filters move to outer WHERE (not inner WHERE) + List innerEntityFieldsFilters = isOr ? Collections.emptyList() : entityFieldsFiltersMapping; + + String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), innerEntityFieldsFilters); + String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true, isOr); + String latestJoinsData = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, false, isOr); + + // Under OR: combine ALL filter mappings (entity fields + aliases) in outer WHERE with OR joiner + String aliasWhereQuery; + if (isOr) { + String combinedFilterQuery = EntityKeyMapping.buildQuery(ctx, filterMapping, query.getEntityFilter().getType(), ComplexOperation.OR); + String searchTextQuery = buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch()); + aliasWhereQuery = ""; + if (!combinedFilterQuery.isEmpty()) { + aliasWhereQuery = " where (" + combinedFilterQuery + ")"; + } + if (!searchTextQuery.isEmpty()) { + aliasWhereQuery += (aliasWhereQuery.isEmpty() ? " where " : " and ") + "(" + searchTextQuery + ") "; + } + } else { + aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, pageLink.getTextSearch()); + } - String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping); - String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true); - String latestJoinsData = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, false); - String aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, pageLink.getTextSearch()); String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); String entityTypeStr; if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { From 02ddfd09a9469636ab3f53a8310e2dda24708db3 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 16:54:30 +0200 Subject: [PATCH 183/313] Add controller integration tests for OR key filter queries - Add testCountEntitiesWithOrKeyFiltersOperation: verifies OR count returns entities matching any filter - Add testFindEntityDataWithOrKeyFiltersOperation: verifies OR data query with different keys - Add testFindEntityDataWithOrSameKeyFilters: verifies same-key OR ungrouping - Add testCountEntitiesWithoutKeyFiltersOperation: verifies null defaults to AND - Add testAlarmDataQueryWithOrKeyFiltersOperation: verifies alarm queries with OR - Add OrConditionsDisabledEntityQueryControllerTest: verifies OR rejected when toggle disabled --- .../controller/EntityQueryControllerTest.java | 325 ++++++++++++++++++ ...ionsDisabledEntityQueryControllerTest.java | 63 ++++ 2 files changed, 388 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 835f00985b..2cf7c85ba9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -62,6 +62,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; @@ -1729,4 +1730,328 @@ public class EntityQueryControllerTest extends AbstractControllerTest { return new EntityDataQuery(filter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), null, null); } + // --- OR conditions tests --- + + @Test + public void testCountEntitiesWithOrKeyFiltersOperation() throws Exception { + // Create 3 devices with different temperature attributes + Device deviceA = new Device(); + deviceA.setName("OrTestDeviceA"); + deviceA.setType("orTestType"); + deviceA = doPost("/api/device", deviceA, Device.class); + String payloadA = "{\"temperature\":60}"; + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, payloadA, String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("OrTestDeviceB"); + deviceB.setType("orTestType"); + deviceB = doPost("/api/device", deviceB, Device.class); + String payloadB = "{\"temperature\":5}"; + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, payloadB, String.class, status().isOk()); + + Device deviceC = new Device(); + deviceC.setName("OrTestDeviceC"); + deviceC.setType("orTestType"); + deviceC = doPost("/api/device", deviceC, Device.class); + String payloadC = "{\"temperature\":30}"; + doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, payloadC, String.class, status().isOk()); + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orTestType")); + filter.setDeviceNameFilter(""); + + // Filter 1: temperature > 50 + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + // Filter 2: temperature < 10 + KeyFilter tempLt10 = new KeyFilter(); + tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempLt10.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lt10 = new NumericFilterPredicate(); + lt10.setValue(FilterPredicateValue.fromDouble(10)); + lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); + tempLt10.setPredicate(lt10); + + List keyFilters = List.of(tempGt50, tempLt10); + + // OR: deviceA (60>50) and deviceB (5<10) match => count=2 + EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + countByQueryAndCheck(orQuery, 2); + + // AND: no device has temperature both >50 AND <10 => count=0 + EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testFindEntityDataWithOrKeyFiltersOperation() throws Exception { + // Create devices with different attributes + Device deviceX = new Device(); + deviceX.setName("OrDataDeviceX"); + deviceX.setType("orDataType"); + deviceX = doPost("/api/device", deviceX, Device.class); + doPost("/api/plugins/telemetry/" + deviceX.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"status\":\"active\"}", String.class, status().isOk()); + + Device deviceY = new Device(); + deviceY.setName("OrDataDeviceY"); + deviceY.setType("orDataType"); + deviceY = doPost("/api/device", deviceY, Device.class); + doPost("/api/plugins/telemetry/" + deviceY.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"humidity\":80}", String.class, status().isOk()); + + Device deviceZ = new Device(); + deviceZ.setName("OrDataDeviceZ"); + deviceZ.setType("orDataType"); + deviceZ = doPost("/api/device", deviceZ, Device.class); + // deviceZ has neither matching attribute + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orDataType")); + filter.setDeviceNameFilter(""); + + // Filter 1: status = "active" + KeyFilter statusFilter = new KeyFilter(); + statusFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "status")); + statusFilter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate statusPred = new StringFilterPredicate(); + statusPred.setValue(FilterPredicateValue.fromString("active")); + statusPred.setOperation(StringFilterPredicate.StringOperation.EQUAL); + statusFilter.setPredicate(statusPred); + + // Filter 2: humidity > 70 + KeyFilter humidityFilter = new KeyFilter(); + humidityFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidity")); + humidityFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate humidityPred = new NumericFilterPredicate(); + humidityPred.setValue(FilterPredicateValue.fromDouble(70)); + humidityPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + humidityFilter.setPredicate(humidityPred); + + List keyFilters = List.of(statusFilter, humidityFilter); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + // OR: deviceX matches status=active, deviceY matches humidity>70 + EntityDataQuery orQuery = new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters, ComplexOperation.OR); + PageData result = findByQueryAndCheck(orQuery, 2); + List names = result.getData().stream() + .map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()) + .collect(Collectors.toList()); + assertThat(names).containsExactlyInAnyOrder("OrDataDeviceX", "OrDataDeviceY"); + } + + @Test + public void testFindEntityDataWithOrSameKeyFilters() throws Exception { + // Create devices with temperature values that test same-key OR ungrouping + Device deviceA = new Device(); + deviceA.setName("OrSameKeyDeviceA"); + deviceA.setType("orSameKeyType"); + deviceA = doPost("/api/device", deviceA, Device.class); + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":60}", String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("OrSameKeyDeviceB"); + deviceB.setType("orSameKeyType"); + deviceB = doPost("/api/device", deviceB, Device.class); + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":5}", String.class, status().isOk()); + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orSameKeyType")); + filter.setDeviceNameFilter(""); + + // Filter 1: temperature > 50 + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + // Filter 2: temperature < 10 (same key, different predicate) + KeyFilter tempLt10 = new KeyFilter(); + tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempLt10.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lt10 = new NumericFilterPredicate(); + lt10.setValue(FilterPredicateValue.fromDouble(10)); + lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); + tempLt10.setPredicate(lt10); + + List keyFilters = List.of(tempGt50, tempLt10); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + // OR on same key: deviceA (60>50) and deviceB (5<10) should both be returned + EntityDataQuery orQuery = new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters, ComplexOperation.OR); + PageData result = findByQueryAndCheck(orQuery, 2); + List names = result.getData().stream() + .map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()) + .collect(Collectors.toList()); + assertThat(names).containsExactlyInAnyOrder("OrSameKeyDeviceA", "OrSameKeyDeviceB"); + } + + @Test + public void testCountEntitiesWithoutKeyFiltersOperation() throws Exception { + // Create 2 devices with temperature attributes + Device deviceA = new Device(); + deviceA.setName("BackCompatDeviceA"); + deviceA.setType("backCompatType"); + deviceA = doPost("/api/device", deviceA, Device.class); + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":60}", String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("BackCompatDeviceB"); + deviceB.setType("backCompatType"); + deviceB = doPost("/api/device", deviceB, Device.class); + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":5}", String.class, status().isOk()); + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("backCompatType")); + filter.setDeviceNameFilter(""); + + // Filter 1: temperature > 50 + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + // Filter 2: temperature < 10 + KeyFilter tempLt10 = new KeyFilter(); + tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempLt10.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lt10 = new NumericFilterPredicate(); + lt10.setValue(FilterPredicateValue.fromDouble(10)); + lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); + tempLt10.setPredicate(lt10); + + List keyFilters = List.of(tempGt50, tempLt10); + + // Query without keyFiltersOperation (null) -- should behave as AND + EntityCountQuery nullOpQuery = new EntityCountQuery(filter, keyFilters); + Long nullResult = countByQueryAndCheck(nullOpQuery, 0); + + // Query with explicit AND -- should produce the same result + EntityCountQuery andOpQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + Long andResult = countByQueryAndCheck(andOpQuery, 0); + + Assert.assertEquals(nullResult, andResult); + } + + @Test + public void testAlarmDataQueryWithOrKeyFiltersOperation() throws Exception { + loginTenantAdmin(); + + // Create devices with different temperatures and alarms + Device deviceHot = new Device(); + deviceHot.setName("OrAlarmDeviceHot"); + deviceHot.setType("orAlarmType"); + deviceHot = doPost("/api/device", deviceHot, Device.class); + doPost("/api/plugins/telemetry/" + deviceHot.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":60}", String.class, status().isOk()); + + Device deviceCold = new Device(); + deviceCold.setName("OrAlarmDeviceCold"); + deviceCold.setType("orAlarmType"); + deviceCold = doPost("/api/device", deviceCold, Device.class); + doPost("/api/plugins/telemetry/" + deviceCold.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":5}", String.class, status().isOk()); + + Device deviceMid = new Device(); + deviceMid.setName("OrAlarmDeviceMid"); + deviceMid.setType("orAlarmType"); + deviceMid = doPost("/api/device", deviceMid, Device.class); + doPost("/api/plugins/telemetry/" + deviceMid.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":30}", String.class, status().isOk()); + + Thread.sleep(500); + + // Create alarms for each device + Alarm alarmHot = new Alarm(); + alarmHot.setOriginator(deviceHot.getId()); + alarmHot.setType("highTemp"); + alarmHot.setSeverity(AlarmSeverity.CRITICAL); + doPost("/api/alarm", alarmHot, Alarm.class); + + Alarm alarmCold = new Alarm(); + alarmCold.setOriginator(deviceCold.getId()); + alarmCold.setType("lowTemp"); + alarmCold.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarmCold, Alarm.class); + + Alarm alarmMid = new Alarm(); + alarmMid.setOriginator(deviceMid.getId()); + alarmMid.setType("normalTemp"); + alarmMid.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarmMid, Alarm.class); + + Thread.sleep(500); + + // Filter 1: temperature > 50 + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + // Filter 2: temperature < 10 + KeyFilter tempLt10 = new KeyFilter(); + tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempLt10.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lt10 = new NumericFilterPredicate(); + lt10.setValue(FilterPredicateValue.fromDouble(10)); + lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); + tempLt10.setPredicate(lt10); + + List keyFilters = List.of(tempGt50, tempLt10); + + DeviceTypeFilter entityFilter = new DeviceTypeFilter(); + entityFilter.setDeviceTypes(List.of("orAlarmType")); + entityFilter.setDeviceNameFilter(""); + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(100); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "createdTime"))); + + List alarmFields = List.of(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); + + // OR query: should return alarms for deviceHot (60>50) and deviceCold (5<10) = 2 alarms + AlarmDataQuery orAlarmQuery = new AlarmDataQuery(entityFilter, pageLink, null, null, keyFilters, alarmFields, ComplexOperation.OR); + PageData alarmResult = findAlarmsByQueryAndCheck(orAlarmQuery, 2); + List alarmTypes = alarmResult.getData().stream().map(AlarmData::getType).collect(Collectors.toList()); + assertThat(alarmTypes).containsExactlyInAnyOrder("highTemp", "lowTemp"); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java new file mode 100644 index 0000000000..4b5d3959b9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import org.junit.Test; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.query.ComplexOperation; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +@DirtiesContext +@TestPropertySource(properties = { + "queue.edqs.sync.enabled=true", + "queue.edqs.api.supported=false", + "sql.query.key-filters-or-conditions.enabled=false" +}) +public class OrConditionsDisabledEntityQueryControllerTest extends EntityQueryControllerTest { + + @Test + public void testOrKeyFiltersOperationRejectedWhenDisabled() throws Exception { + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("default")); + filter.setDeviceNameFilter(""); + + // POST a query with OR operation -- should be rejected with 400 + EntityCountQuery orQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.OR); + String errorMessage = getErrorMessage( + doPost("/api/entitiesQuery/count", orQuery).andExpect(status().isBadRequest()) + ); + assertThat(errorMessage).contains("OR conditions between key filters are disabled"); + + // POST a query without keyFiltersOperation (null/AND) -- should still succeed + EntityCountQuery andQuery = new EntityCountQuery(filter, Collections.emptyList()); + doPost("/api/entitiesQuery/count", andQuery).andExpect(status().isOk()); + + // POST a query with explicit AND -- should also succeed + EntityCountQuery explicitAndQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.AND); + doPost("/api/entitiesQuery/count", explicitAndQuery).andExpect(status().isOk()); + } + +} From adeacba23f62f01a0340bc0ae253135beddbe015 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 16:56:05 +0200 Subject: [PATCH 184/313] Add EDQS unit tests for OR filter evaluation in RepositoryUtils - Add testCheckKeyFiltersWithOrOperation: OR short-circuits on first match - Add testCheckKeyFiltersWithOrOperationNoMatch: OR returns false when nothing matches - Add testCheckKeyFiltersWithOrOperationSameKey: same-key OR works correctly - Add testCheckKeyFiltersWithAndOperationUnchanged: AND behavior unchanged - Add testCheckKeyFiltersWithNullOperationDefaultsToAnd: null defaults to AND - Add testCheckKeyFiltersWithOrEmptyFilters: OR with empty filters returns true --- .../server/edqs/repo/RepositoryUtilsTest.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java index 557cfb7a4a..a7846c0674 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.edqs.repo; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -44,6 +45,7 @@ import org.thingsboard.server.edqs.query.DataKey; import org.thingsboard.server.edqs.query.EdqsFilter; import org.thingsboard.server.edqs.util.RepositoryUtils; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.stream.Stream; @@ -469,4 +471,118 @@ public class RepositoryUtilsTest { return complexFilterPredicate; } + // --- OR operation tests for checkKeyFilters --- + + @Test + public void testCheckKeyFiltersWithOrOperation() { + // Entity with temperature=60; filters: temperature>50 (match) and humidity>80 (no data) + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("orDevice1").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter humidityGt80 = getHumidityFilter(NumericOperation.GREATER, 80); + + // OR: temperature>50 matches, short-circuits to true + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, humidityGt80), ComplexOperation.OR)).isTrue(); + } + + @Test + public void testCheckKeyFiltersWithOrOperationNoMatch() { + // Entity with temperature=30; filters: temperature>50 (no match) and temperature<10 (no match) + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("orDevice2").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 30.0)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter tempLt10 = getTemperatureFilter(NumericOperation.LESS, 10); + + // OR: neither filter matches + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, tempLt10), ComplexOperation.OR)).isFalse(); + } + + @Test + public void testCheckKeyFiltersWithOrOperationSameKey() { + // Entity with temperature=5; filters: temperature>50 (no match) and temperature<10 (match) + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("orDevice3").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 5.0)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter tempLt10 = getTemperatureFilter(NumericOperation.LESS, 10); + + // OR on same key: second filter matches + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, tempLt10), ComplexOperation.OR)).isTrue(); + } + + @Test + public void testCheckKeyFiltersWithAndOperationUnchanged() { + // Entity with temperature=60 and humidity=90 + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("andDevice1").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + deviceData.putTs(6, new LongDataPoint(System.currentTimeMillis(), 90)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter humidityGt80 = getHumidityFilter(NumericOperation.GREATER, 80); + + // AND: both match + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, humidityGt80), ComplexOperation.AND)).isTrue(); + + // Now with humidity=70 -- humidity>80 fails + DeviceData deviceData2 = new DeviceData(UUID.randomUUID()); + deviceData2.setCustomerId(UUID.randomUUID()); + deviceData2.setFields(DeviceFields.builder().name("andDevice2").build()); + deviceData2.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + deviceData2.putTs(6, new LongDataPoint(System.currentTimeMillis(), 70)); + + // AND: humidity>80 fails + assertThat(RepositoryUtils.checkKeyFilters(deviceData2, List.of(tempGt50, humidityGt80), ComplexOperation.AND)).isFalse(); + } + + @Test + public void testCheckKeyFiltersWithNullOperationDefaultsToAnd() { + // Entity with temperature=60 and humidity=90 + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("nullOpDevice").build()); + deviceData.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + deviceData.putTs(6, new LongDataPoint(System.currentTimeMillis(), 90)); + + EdqsFilter tempGt50 = getTemperatureFilter(NumericOperation.GREATER, 50); + EdqsFilter humidityGt80 = getHumidityFilter(NumericOperation.GREATER, 80); + + // null operation should behave like AND + boolean nullResult = RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, humidityGt80), null); + boolean andResult = RepositoryUtils.checkKeyFilters(deviceData, List.of(tempGt50, humidityGt80), ComplexOperation.AND); + assertThat(nullResult).isEqualTo(andResult); + assertThat(nullResult).isTrue(); + + // Entity with temperature=60 and humidity=70 -- humidity>80 fails + DeviceData deviceData2 = new DeviceData(UUID.randomUUID()); + deviceData2.setCustomerId(UUID.randomUUID()); + deviceData2.setFields(DeviceFields.builder().name("nullOpDevice2").build()); + deviceData2.putTs(5, new DoubleDataPoint(System.currentTimeMillis(), 60.0)); + deviceData2.putTs(6, new LongDataPoint(System.currentTimeMillis(), 70)); + + boolean nullResult2 = RepositoryUtils.checkKeyFilters(deviceData2, List.of(tempGt50, humidityGt80), null); + boolean andResult2 = RepositoryUtils.checkKeyFilters(deviceData2, List.of(tempGt50, humidityGt80), ComplexOperation.AND); + assertThat(nullResult2).isEqualTo(andResult2); + assertThat(nullResult2).isFalse(); + } + + @Test + public void testCheckKeyFiltersWithOrEmptyFilters() { + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("emptyFilterDevice").build()); + + // OR with empty filters = include all (matches SQL behavior) + assertThat(RepositoryUtils.checkKeyFilters(deviceData, Collections.emptyList(), ComplexOperation.OR)).isTrue(); + } + } From ca08f4b435fec0376a01c5e6aa98dc746a0a5096 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Mar 2026 18:20:28 +0200 Subject: [PATCH 185/313] Fix OR key filter SQL generation, propagation, and add comprehensive tests - Move OR filter predicates to middle-layer WHERE clause where JOIN alias table references are visible, fixing BadSqlGrammarException - Use alias name (e.g. alias2) instead of bare column name for entity field predicates in middle-layer WHERE - Propagate keyFiltersOperation through BaseEntityService optimization path and alarm subscription contexts (TbAlarmDataSubCtx, TbAlarmCountSubCtx) - Replace ThingsboardException with IllegalArgumentException for OR disabled validation - Add AlarmCountQuery 3-arg constructor for test convenience - Add 8 new integration tests: alarm data/count OR, mixed entity field + attribute OR, string attributes, single filter, 3-way OR, pagination, and zero-match edge case --- .../query/DefaultEntityQueryService.java | 12 +- .../service/query/EntityQueryService.java | 9 +- .../subscription/TbAlarmCountSubCtx.java | 2 +- .../subscription/TbAlarmDataSubCtx.java | 2 +- .../controller/EntityQueryControllerTest.java | 435 ++++++++++++++++++ .../common/data/query/AlarmCountQuery.java | 4 + .../server/dao/entity/BaseEntityService.java | 2 +- .../query/DefaultEntityQueryRepository.java | 24 +- .../dao/sql/query/EntityKeyMapping.java | 12 +- 9 files changed, 474 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 00426dae18..5883920ad8 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -107,13 +107,13 @@ public class DefaultEntityQueryService implements EntityQueryService { private AttributesService attributesService; @Override - public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) throws ThingsboardException { + public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) { validateKeyFiltersOperation(query); return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); } @Override - public PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) throws ThingsboardException { + public PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) { validateKeyFiltersOperation(query); if (query.getKeyFilters() != null) { resolveDynamicValuesInPredicates( @@ -183,7 +183,7 @@ public class DefaultEntityQueryService implements EntityQueryService { } @Override - public PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) throws ThingsboardException { + public PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) { validateKeyFiltersOperation(query); EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); PageData entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), @@ -210,7 +210,7 @@ public class DefaultEntityQueryService implements EntityQueryService { } @Override - public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) throws ThingsboardException { + public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) { validateKeyFiltersOperation(query); if (query.getEntityFilter() != null) { EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); @@ -244,9 +244,9 @@ public class DefaultEntityQueryService implements EntityQueryService { return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } - private void validateKeyFiltersOperation(EntityCountQuery query) throws ThingsboardException { + private void validateKeyFiltersOperation(EntityCountQuery query) { if (!keyFiltersOrConditionsEnabled && query.getKeyFiltersOperation() == ComplexOperation.OR) { - throw new ThingsboardException("OR conditions between key filters are disabled", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + throw new IllegalArgumentException("OR conditions between key filters are disabled"); } } diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java index b3d2f208ca..354f8e9278 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.query; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; @@ -34,13 +33,13 @@ import java.util.Set; public interface EntityQueryService { - long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) throws ThingsboardException; + long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query); - PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) throws ThingsboardException; + PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query); - PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) throws ThingsboardException; + PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query); - long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) throws ThingsboardException; + long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query); ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, boolean isTimeseries, boolean isAttributes, AttributeScope scope); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java index c2fe743bb4..22fdd77d7a 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -130,7 +130,7 @@ public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx { entitiesSortOrder = sortOrder; } EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); - return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperation()); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 2cf7c85ba9..cee923346b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -78,6 +78,7 @@ import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter; @@ -2052,6 +2053,440 @@ public class EntityQueryControllerTest extends AbstractControllerTest { PageData alarmResult = findAlarmsByQueryAndCheck(orAlarmQuery, 2); List alarmTypes = alarmResult.getData().stream().map(AlarmData::getType).collect(Collectors.toList()); assertThat(alarmTypes).containsExactlyInAnyOrder("highTemp", "lowTemp"); + + // AND query: no device has temp both >50 AND <10 => 0 alarms + AlarmDataQuery andAlarmQuery = new AlarmDataQuery(entityFilter, pageLink, null, null, keyFilters, alarmFields, ComplexOperation.AND); + findAlarmsByQueryAndCheck(andAlarmQuery, 0); + } + + @Test + public void testCountAlarmsByQueryWithOrKeyFiltersOperation() throws Exception { + loginTenantAdmin(); + + Device deviceHot = new Device(); + deviceHot.setName("OrAlarmCntDeviceHot"); + deviceHot.setType("orAlarmCntType"); + deviceHot = doPost("/api/device", deviceHot, Device.class); + doPost("/api/plugins/telemetry/" + deviceHot.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":60}", String.class, status().isOk()); + + Device deviceCold = new Device(); + deviceCold.setName("OrAlarmCntDeviceCold"); + deviceCold.setType("orAlarmCntType"); + deviceCold = doPost("/api/device", deviceCold, Device.class); + doPost("/api/plugins/telemetry/" + deviceCold.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":5}", String.class, status().isOk()); + + Device deviceMid = new Device(); + deviceMid.setName("OrAlarmCntDeviceMid"); + deviceMid.setType("orAlarmCntType"); + deviceMid = doPost("/api/device", deviceMid, Device.class); + doPost("/api/plugins/telemetry/" + deviceMid.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":30}", String.class, status().isOk()); + + Thread.sleep(500); + + // Create 2 alarms for deviceHot, 1 for deviceCold, 1 for deviceMid + Alarm alarm1 = new Alarm(); + alarm1.setOriginator(deviceHot.getId()); + alarm1.setType("highTemp1"); + alarm1.setSeverity(AlarmSeverity.CRITICAL); + doPost("/api/alarm", alarm1, Alarm.class); + + Alarm alarm2 = new Alarm(); + alarm2.setOriginator(deviceHot.getId()); + alarm2.setType("highTemp2"); + alarm2.setSeverity(AlarmSeverity.CRITICAL); + doPost("/api/alarm", alarm2, Alarm.class); + + Alarm alarm3 = new Alarm(); + alarm3.setOriginator(deviceCold.getId()); + alarm3.setType("lowTemp"); + alarm3.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm3, Alarm.class); + + Alarm alarm4 = new Alarm(); + alarm4.setOriginator(deviceMid.getId()); + alarm4.setType("normalTemp"); + alarm4.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm4, Alarm.class); + + Thread.sleep(500); + + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + KeyFilter tempLt10 = new KeyFilter(); + tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempLt10.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lt10 = new NumericFilterPredicate(); + lt10.setValue(FilterPredicateValue.fromDouble(10)); + lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); + tempLt10.setPredicate(lt10); + + List keyFilters = List.of(tempGt50, tempLt10); + + DeviceTypeFilter entityFilter = new DeviceTypeFilter(); + entityFilter.setDeviceTypes(List.of("orAlarmCntType")); + entityFilter.setDeviceNameFilter(""); + + // OR: deviceHot (2 alarms) + deviceCold (1 alarm) match => 3 alarms total + AlarmCountQuery orQuery = new AlarmCountQuery(entityFilter, keyFilters, ComplexOperation.OR); + countAlarmsByQueryAndCheck(orQuery, 3); + + // AND: no device matches both filters => 0 + AlarmCountQuery andQuery = new AlarmCountQuery(entityFilter, keyFilters, ComplexOperation.AND); + countAlarmsByQueryAndCheck(andQuery, 0); + } + + @Test + public void testCountEntitiesWithOrMixedEntityFieldAndAttribute() throws Exception { + // Tests the entity field predicate relocation to middle-layer WHERE under OR + Device deviceA = new Device(); + deviceA.setName("OrMixedAlpha"); + deviceA.setType("orMixedType"); + deviceA = doPost("/api/device", deviceA, Device.class); + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":10}", String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("OrMixedBeta"); + deviceB.setType("orMixedType"); + deviceB = doPost("/api/device", deviceB, Device.class); + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":60}", String.class, status().isOk()); + + Device deviceC = new Device(); + deviceC.setName("OrMixedGamma"); + deviceC.setType("orMixedType"); + deviceC = doPost("/api/device", deviceC, Device.class); + doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":10}", String.class, status().isOk()); + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orMixedType")); + filter.setDeviceNameFilter(""); + + // Filter 1: entity field name CONTAINS "Alpha" + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + nameFilter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate namePred = new StringFilterPredicate(); + namePred.setValue(FilterPredicateValue.fromString("Alpha")); + namePred.setOperation(StringFilterPredicate.StringOperation.CONTAINS); + nameFilter.setPredicate(namePred); + + // Filter 2: attribute temperature > 50 + KeyFilter tempFilter = new KeyFilter(); + tempFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate tempPred = new NumericFilterPredicate(); + tempPred.setValue(FilterPredicateValue.fromDouble(50)); + tempPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempFilter.setPredicate(tempPred); + + List keyFilters = List.of(nameFilter, tempFilter); + + // OR: deviceA matches name contains "Alpha", deviceB matches temp>50 => count=2 + EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + countByQueryAndCheck(orQuery, 2); + + // AND: only deviceA has name "Alpha" AND temp is 10 (not >50) => count=0 + EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testCountEntitiesWithOrStringAttributes() throws Exception { + Device deviceA = new Device(); + deviceA.setName("OrStrDeviceA"); + deviceA.setType("orStrType"); + deviceA = doPost("/api/device", deviceA, Device.class); + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"color\":\"red\"}", String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("OrStrDeviceB"); + deviceB.setType("orStrType"); + deviceB = doPost("/api/device", deviceB, Device.class); + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"color\":\"blue\"}", String.class, status().isOk()); + + Device deviceC = new Device(); + deviceC.setName("OrStrDeviceC"); + deviceC.setType("orStrType"); + deviceC = doPost("/api/device", deviceC, Device.class); + doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"color\":\"green\"}", String.class, status().isOk()); + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orStrType")); + filter.setDeviceNameFilter(""); + + // Filter 1: color = "red" + KeyFilter redFilter = new KeyFilter(); + redFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "color")); + redFilter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate redPred = new StringFilterPredicate(); + redPred.setValue(FilterPredicateValue.fromString("red")); + redPred.setOperation(StringFilterPredicate.StringOperation.EQUAL); + redFilter.setPredicate(redPred); + + // Filter 2: color = "blue" + KeyFilter blueFilter = new KeyFilter(); + blueFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "color")); + blueFilter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate bluePred = new StringFilterPredicate(); + bluePred.setValue(FilterPredicateValue.fromString("blue")); + bluePred.setOperation(StringFilterPredicate.StringOperation.EQUAL); + blueFilter.setPredicate(bluePred); + + List keyFilters = List.of(redFilter, blueFilter); + + // OR: deviceA (red) and deviceB (blue) match => count=2 + EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + countByQueryAndCheck(orQuery, 2); + + // AND: no device is both red AND blue => count=0 + EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testCountEntitiesWithOrSingleFilter() throws Exception { + Device deviceA = new Device(); + deviceA.setName("OrSingleDeviceA"); + deviceA.setType("orSingleType"); + deviceA = doPost("/api/device", deviceA, Device.class); + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":60}", String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("OrSingleDeviceB"); + deviceB.setType("orSingleType"); + deviceB = doPost("/api/device", deviceB, Device.class); + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":30}", String.class, status().isOk()); + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orSingleType")); + filter.setDeviceNameFilter(""); + + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + List keyFilters = List.of(tempGt50); + + // Single filter with OR should behave identically to AND + EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + Long orResult = countByQueryAndCheck(orQuery, 1); + + EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + Long andResult = countByQueryAndCheck(andQuery, 1); + + Assert.assertEquals(orResult, andResult); + } + + @Test + public void testCountEntitiesWithOrThreeFilters() throws Exception { + Device deviceA = new Device(); + deviceA.setName("Or3fDeviceA"); + deviceA.setType("or3fType"); + deviceA = doPost("/api/device", deviceA, Device.class); + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":60,\"humidity\":50,\"pressure\":1000}", String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("Or3fDeviceB"); + deviceB.setType("or3fType"); + deviceB = doPost("/api/device", deviceB, Device.class); + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":20,\"humidity\":90,\"pressure\":1000}", String.class, status().isOk()); + + Device deviceC = new Device(); + deviceC.setName("Or3fDeviceC"); + deviceC.setType("or3fType"); + deviceC = doPost("/api/device", deviceC, Device.class); + doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":20,\"humidity\":50,\"pressure\":1050}", String.class, status().isOk()); + + Device deviceD = new Device(); + deviceD.setName("Or3fDeviceD"); + deviceD.setType("or3fType"); + deviceD = doPost("/api/device", deviceD, Device.class); + doPost("/api/plugins/telemetry/" + deviceD.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":20,\"humidity\":50,\"pressure\":1000}", String.class, status().isOk()); + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("or3fType")); + filter.setDeviceNameFilter(""); + + // Filter 1: temperature > 50 + KeyFilter tempFilter = new KeyFilter(); + tempFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate tempPred = new NumericFilterPredicate(); + tempPred.setValue(FilterPredicateValue.fromDouble(50)); + tempPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempFilter.setPredicate(tempPred); + + // Filter 2: humidity > 80 + KeyFilter humFilter = new KeyFilter(); + humFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidity")); + humFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate humPred = new NumericFilterPredicate(); + humPred.setValue(FilterPredicateValue.fromDouble(80)); + humPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + humFilter.setPredicate(humPred); + + // Filter 3: pressure > 1040 + KeyFilter pressFilter = new KeyFilter(); + pressFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "pressure")); + pressFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate pressPred = new NumericFilterPredicate(); + pressPred.setValue(FilterPredicateValue.fromDouble(1040)); + pressPred.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + pressFilter.setPredicate(pressPred); + + List keyFilters = List.of(tempFilter, humFilter, pressFilter); + + // OR: A matches temp>50, B matches hum>80, C matches press>1040, D matches none => 3 + EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + countByQueryAndCheck(orQuery, 3); + + // AND: no device matches all three => 0 + EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testFindEntityDataWithOrPagination() throws Exception { + // Create 5 devices, 4 match OR filters (1,2: temp>50; 3,4: temp<10; 5: no match) + for (int i = 1; i <= 5; i++) { + Device device = new Device(); + device.setName(String.format("OrPageDevice%02d", i)); + device.setType("orPageType"); + device = doPost("/api/device", device, Device.class); + // Devices 1,2: temperature > 50. Device 3,4: temperature < 10. Device 5: no match (25). + int temp = (i <= 2) ? 60 + i : (i <= 4) ? 5 - i : 25; + doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":" + temp + "}", String.class, status().isOk()); + } + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orPageType")); + filter.setDeviceNameFilter(""); + + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + KeyFilter tempLt10 = new KeyFilter(); + tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempLt10.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lt10 = new NumericFilterPredicate(); + lt10.setValue(FilterPredicateValue.fromDouble(10)); + lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); + tempLt10.setPredicate(lt10); + + List keyFilters = List.of(tempGt50, tempLt10); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC + ); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + // Page 1: pageSize=2, totalElements=4, data.size()=2 + EntityDataPageLink pageLink1 = new EntityDataPageLink(2, 0, null, sortOrder); + EntityDataQuery orQuery1 = new EntityDataQuery(filter, pageLink1, entityFields, null, keyFilters, ComplexOperation.OR); + PageData page1 = findByQueryAndCheck(orQuery1, 4); + Assert.assertEquals(2, page1.getData().size()); + Assert.assertTrue(page1.hasNext()); + + // Page 2: remaining 2 of 4 + EntityDataPageLink pageLink2 = new EntityDataPageLink(2, 1, null, sortOrder); + EntityDataQuery orQuery2 = new EntityDataQuery(filter, pageLink2, entityFields, null, keyFilters, ComplexOperation.OR); + PageData page2 = findByQueryAndCheck(orQuery2, 4); + Assert.assertEquals(2, page2.getData().size()); + Assert.assertFalse(page2.hasNext()); + + // All names across both pages should be from the 4 matching devices (01,02,03,04) + List allNames = new ArrayList<>(); + page1.getData().forEach(e -> allNames.add(e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue())); + page2.getData().forEach(e -> allNames.add(e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue())); + assertThat(allNames).hasSize(4); + assertThat(allNames).doesNotContain("OrPageDevice05"); + } + + @Test + public void testCountEntitiesWithOrZeroMatches() throws Exception { + Device deviceA = new Device(); + deviceA.setName("OrZeroDeviceA"); + deviceA.setType("orZeroType"); + deviceA = doPost("/api/device", deviceA, Device.class); + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":30}", String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("OrZeroDeviceB"); + deviceB.setType("orZeroType"); + deviceB = doPost("/api/device", deviceB, Device.class); + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":40}", String.class, status().isOk()); + + Thread.sleep(1000); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orZeroType")); + filter.setDeviceNameFilter(""); + + // Filter 1: temperature > 50 (no match) + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + // Filter 2: temperature < 10 (no match) + KeyFilter tempLt10 = new KeyFilter(); + tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempLt10.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lt10 = new NumericFilterPredicate(); + lt10.setValue(FilterPredicateValue.fromDouble(10)); + lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); + tempLt10.setPredicate(lt10); + + List keyFilters = List.of(tempGt50, tempLt10); + + // OR with no matches: neither filter matches any device => count=0 + EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + countByQueryAndCheck(orQuery, 0); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java index 8431d7a303..d45335021f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java @@ -51,4 +51,8 @@ public class AlarmCountQuery extends EntityCountQuery { super(entityFilter, keyFilters); } + public AlarmCountQuery(EntityFilter entityFilter, List keyFilters, ComplexOperation keyFiltersOperation) { + super(entityFilter, keyFilters, keyFiltersOperation); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index b20535dbf4..e1b3b084ec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -409,7 +409,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe .collect(Collectors.toList()); } } - EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters()); + EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters(), query.getKeyFiltersOperation()); return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 94e615a1bb..140fe38af4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -386,11 +386,16 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), innerEntityFieldsFilters); String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true, isOr); - // Under OR: combine ALL filter mappings (entity fields + aliases) in outer WHERE with OR joiner + // Under OR: combine ALL filter mappings (entity fields + aliases) with OR joiner + // Place at the middle layer (after JOINs) where alias table references are visible. + // Under AND: place at the outer result layer (existing behavior). String aliasWhereQuery; if (isOr) { String combinedFilterQuery = EntityKeyMapping.buildQuery(ctx, filterMapping, query.getEntityFilter().getType(), ComplexOperation.OR); - aliasWhereQuery = combinedFilterQuery.isEmpty() ? "" : " where (" + combinedFilterQuery + ")"; + if (!combinedFilterQuery.isEmpty()) { + latestJoinsCnt += " where (" + combinedFilterQuery + ")"; + } + aliasWhereQuery = ""; } else { aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, ""); } @@ -481,18 +486,23 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true, isOr); String latestJoinsData = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, false, isOr); - // Under OR: combine ALL filter mappings (entity fields + aliases) in outer WHERE with OR joiner + // Under OR: combine ALL filter mappings (entity fields + aliases) with OR joiner + // Place at the middle layer (after JOINs) where alias table references are visible. + // Under AND: place at the outer result layer (existing behavior). String aliasWhereQuery; if (isOr) { String combinedFilterQuery = EntityKeyMapping.buildQuery(ctx, filterMapping, query.getEntityFilter().getType(), ComplexOperation.OR); - String searchTextQuery = buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch()); - aliasWhereQuery = ""; + String middleWhere = ""; if (!combinedFilterQuery.isEmpty()) { - aliasWhereQuery = " where (" + combinedFilterQuery + ")"; + middleWhere = " where (" + combinedFilterQuery + ")"; } + String searchTextQuery = buildTextSearchQuery(ctx, selectionMapping, pageLink.getTextSearch()); if (!searchTextQuery.isEmpty()) { - aliasWhereQuery += (aliasWhereQuery.isEmpty() ? " where " : " and ") + "(" + searchTextQuery + ") "; + middleWhere += (middleWhere.isEmpty() ? " where " : " and ") + "(" + searchTextQuery + ") "; } + latestJoinsCnt += middleWhere; + latestJoinsData += middleWhere; + aliasWhereQuery = ""; } else { aliasWhereQuery = DefaultEntityQueryRepository.this.buildAliasWhereQuery(ctx, query.getEntityFilter(), selectionMapping, pageLink.getTextSearch()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 72096dbf6e..28fa6bec0f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -292,16 +292,14 @@ public class EntityKeyMapping { String keyAlias; if (entityKey.getType().equals(EntityKeyType.ENTITY_FIELD) && getEntityKeyColumn() != null) { if (outerContext) { - // In the outer query (OR relocation), entity field columns are available - // as bare column names from the inner subquery SELECT. - // buildSimplePredicateQuery does: alias + "." + entityKeyColumn when entityKeyColumn != null. - // To produce just the bare column name, temporarily null out entityKeyColumn - // so buildSimplePredicateQuery uses alias directly as the field. - String bareColumn = getEntityKeyColumn(); + // In the middle layer (OR relocation), entity field columns are available + // by their alias name from the inner subquery SELECT (e.g., "alias2" from + // "cast(e.name as varchar) as alias2"). Temporarily null out entityKeyColumn + // so buildSimplePredicateQuery uses the alias directly as the field. String savedColumn = this.entityKeyColumn; this.entityKeyColumn = null; List predicates = keyFilters.stream() - .map(keyFilter -> this.buildKeyQuery(ctx, bareColumn, keyFilter, filterType)) + .map(keyFilter -> this.buildKeyQuery(ctx, alias, keyFilter, filterType)) .collect(Collectors.toList()); this.entityKeyColumn = savedColumn; return predicates.stream(); From 293126caeaac0a4edf57898a68d58491a6addd92 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 10:33:57 +0200 Subject: [PATCH 186/313] Fix test for disabled OR conditions --- .../query/DefaultEntityQueryService.java | 2 + .../controller/EntityQueryControllerTest.java | 35 ++++++++++- ...ionsDisabledEntityQueryControllerTest.java | 63 ------------------- 3 files changed, 35 insertions(+), 65 deletions(-) delete mode 100644 application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 5883920ad8..8da82a54d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.query; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -94,6 +95,7 @@ public class DefaultEntityQueryService implements EntityQueryService { @Value("${server.ws.max_entities_per_alarm_subscription:1000}") private int maxEntitiesPerAlarmSubscription; + @Setter @Value("${sql.query.key-filters-or-conditions.enabled:true}") private boolean keyFiltersOrConditionsEnabled; diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index cee923346b..bf9762b208 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -78,7 +78,6 @@ import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; -import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter; @@ -94,6 +93,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.edqs.util.EdqsRocksDb; +import org.thingsboard.server.service.query.DefaultEntityQueryService; import java.util.ArrayList; import java.util.Arrays; @@ -125,6 +125,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest { @Autowired private QueueStatsService queueStatsService; + @Autowired + private DefaultEntityQueryService entityQueryService; @MockitoBean private EdqsRocksDb edqsRocksDb; @@ -1345,7 +1347,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { //assign dashboard doPost("/api/customer/" + savedCustomer.getId().getId().toString() - + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); // check entity data query by customer User customerUser = new User(); @@ -2489,4 +2491,33 @@ public class EntityQueryControllerTest extends AbstractControllerTest { countByQueryAndCheck(orQuery, 0); } + @Test + public void testOrKeyFiltersOperationRejectedWhenDisabled() throws Exception { + loginTenantAdmin(); + + entityQueryService.setKeyFiltersOrConditionsEnabled(false); + try { + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("default")); + filter.setDeviceNameFilter(""); + + // POST a query with OR operation -- should be rejected with 400 + EntityCountQuery orQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.OR); + String errorMessage = getErrorMessage( + doPost("/api/entitiesQuery/count", orQuery).andExpect(status().isBadRequest()) + ); + assertThat(errorMessage).contains("OR conditions between key filters are disabled"); + + // POST a query without keyFiltersOperation (null/AND) -- should still succeed + EntityCountQuery andQuery = new EntityCountQuery(filter, Collections.emptyList()); + doPost("/api/entitiesQuery/count", andQuery).andExpect(status().isOk()); + + // POST a query with explicit AND -- should also succeed + EntityCountQuery explicitAndQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.AND); + doPost("/api/entitiesQuery/count", explicitAndQuery).andExpect(status().isOk()); + } finally { + entityQueryService.setKeyFiltersOrConditionsEnabled(true); + } + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java deleted file mode 100644 index 4b5d3959b9..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/OrConditionsDisabledEntityQueryControllerTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright © 2016-2026 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.controller; - -import org.junit.Test; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.TestPropertySource; -import org.thingsboard.server.common.data.query.ComplexOperation; -import org.thingsboard.server.common.data.query.DeviceTypeFilter; -import org.thingsboard.server.common.data.query.EntityCountQuery; -import org.thingsboard.server.dao.service.DaoSqlTest; - -import java.util.Collections; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@DaoSqlTest -@DirtiesContext -@TestPropertySource(properties = { - "queue.edqs.sync.enabled=true", - "queue.edqs.api.supported=false", - "sql.query.key-filters-or-conditions.enabled=false" -}) -public class OrConditionsDisabledEntityQueryControllerTest extends EntityQueryControllerTest { - - @Test - public void testOrKeyFiltersOperationRejectedWhenDisabled() throws Exception { - DeviceTypeFilter filter = new DeviceTypeFilter(); - filter.setDeviceTypes(List.of("default")); - filter.setDeviceNameFilter(""); - - // POST a query with OR operation -- should be rejected with 400 - EntityCountQuery orQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.OR); - String errorMessage = getErrorMessage( - doPost("/api/entitiesQuery/count", orQuery).andExpect(status().isBadRequest()) - ); - assertThat(errorMessage).contains("OR conditions between key filters are disabled"); - - // POST a query without keyFiltersOperation (null/AND) -- should still succeed - EntityCountQuery andQuery = new EntityCountQuery(filter, Collections.emptyList()); - doPost("/api/entitiesQuery/count", andQuery).andExpect(status().isOk()); - - // POST a query with explicit AND -- should also succeed - EntityCountQuery explicitAndQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.AND); - doPost("/api/entitiesQuery/count", explicitAndQuery).andExpect(status().isOk()); - } - -} From 16a92eefa088e9939bfef41ea21d59eee26f33bf Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 11:36:14 +0200 Subject: [PATCH 187/313] Update TypeScript models for OR conditions support - Add allowKeyFiltersOrConditions to SysParamsState interface - Add keyFiltersOperation to FilterInfo and EntityCountQuery interfaces - Update keyFiltersToText to accept dynamic operation parameter with AND default - Propagate keyFiltersOperation in getFilterId helper --- ui-ngx/src/app/core/auth/auth.models.ts | 1 + ui-ngx/src/app/shared/models/query/query.models.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 623ceefadd..d353ae8c8c 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -40,6 +40,7 @@ export interface SysParamsState { calculatedFieldDebugPerTenantLimitsConfiguration?: string; intermediateAggregationIntervalInSecForCF: number; trendzSettings: TrendzSettings; + allowKeyFiltersOrConditions: boolean; } export interface SysParams extends SysParamsState { diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 7f04f07664..6273fbd434 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -401,20 +401,22 @@ export interface FilterInfo { filter: string; editable: boolean; keyFilters: Array; + keyFiltersOperation?: ComplexOperation; } export interface FiltersInfo { datasourceFilters: {[datasourceIndex: number]: FilterInfo}; } -export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array): string { +export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array, + operation: ComplexOperation = ComplexOperation.AND): string { const filtersText = keyFilters.map(keyFilter => keyFilterToText(translate, datePipe, keyFilter, - keyFilters.length > 1 ? ComplexOperation.AND : undefined)); + keyFilters.length > 1 ? operation : undefined)); let result: string; if (filtersText.length > 1) { - const andText = translate.instant('filter.operation.and'); - result = filtersText.join(' ' + andText + ' '); + const opText = translate.instant(complexOperationTranslationMap.get(operation)); + result = filtersText.join(' ' + opText + ' '); } else { result = filtersText[0]; } @@ -802,6 +804,7 @@ export const singleEntityFilterFromDeviceId = (deviceId: string): EntityFilter = export interface EntityCountQuery { entityFilter: EntityFilter; keyFilters?: Array; + keyFiltersOperation?: ComplexOperation; } export interface AbstractDataQuery extends EntityCountQuery { @@ -940,7 +943,8 @@ export const getFilterId = (filters: Filters, filterInfo: FilterInfo): string => const newFilterName = createFilterName(filters, filterInfo.filter); newFilterId = guid(); filters[newFilterId] = {id: newFilterId, filter: newFilterName, - keyFilters: filterInfo.keyFilters, editable: filterInfo.editable}; + keyFilters: filterInfo.keyFilters, editable: filterInfo.editable, + keyFiltersOperation: filterInfo.keyFiltersOperation}; } return newFilterId; } From 5672c6812ba15d5e1370b1d2545ab1743164b413 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 11:39:25 +0200 Subject: [PATCH 188/313] Add AND/OR toggle to filter dialog and dynamic operation labels - Add tb-toggle-select with AND/OR options to filter-dialog - Toggle disabled with tooltip when feature flag is false - Pass operation to key-filter-list via @Input binding - Replace hardcoded AND label with dynamic operation label - Pass operation through to filter-text preview component - Add or-conditions-disabled translation key --- .../components/filter/filter-dialog.component.html | 13 ++++++++++++- .../components/filter/filter-dialog.component.ts | 12 ++++++++++-- .../home/components/filter/filter-text.component.ts | 7 +++++-- .../filter/key-filter-list.component.html | 4 ++-- .../components/filter/key-filter-list.component.ts | 6 ++++++ ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 6 files changed, 36 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html index 230c8c8bb7..581dd42c93 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html @@ -48,8 +48,19 @@
+
+
filter.key-filters
+ + {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} + {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }} + +
+ formControlName="keyFilters" + [operation]="filterFormGroup.get('keyFiltersOperation').value">
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts index ec3c9b79f0..465b87b4b1 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts @@ -32,7 +32,8 @@ import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import { Filter, Filters } from '@shared/models/query/query.models'; +import { ComplexOperation, complexOperationTranslationMap, Filter, Filters } from '@shared/models/query/query.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; export interface FilterDialogData { isAdd: boolean; @@ -59,6 +60,10 @@ export class FilterDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: FilterDialogData, @@ -87,11 +92,13 @@ export class FilterDialogComponent extends DialogComponent) { this.requiredClass = false; if (value && value.length) { - this.filterText = keyFiltersToText(this.translate, this.datePipe, value); + this.filterText = keyFiltersToText(this.translate, this.datePipe, value, this.operation); } else { if (this.required && !this.disabled) { this.filterText = this.addFilterPrompt; diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html index ba5fbfc250..a772c765fd 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html @@ -38,7 +38,7 @@ formArrayName="keyFilters" *ngFor="let keyFilterControl of keyFiltersFormArray.controls; let $index = index">
- filter.operation.and + {{ complexOperationTranslationMap.get(operation) | translate }}
@@ -85,7 +85,7 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts index aad686707d..8951764a4b 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts @@ -30,6 +30,8 @@ import { } from '@angular/forms'; import { Observable, Subject } from 'rxjs'; import { + ComplexOperation, + complexOperationTranslationMap, EntityKeyType, entityKeyTypeTranslationMap, KeyFilterInfo, @@ -71,6 +73,10 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator, @Input() entityId: EntityId; + @Input() operation: ComplexOperation = ComplexOperation.AND; + + complexOperationTranslationMap = complexOperationTranslationMap; + keyFilterListFormGroup: UntypedFormGroup; entityKeyTypeTranslations = entityKeyTypeTranslationMap; 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 4fdc707c1e..1f4da168c7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3610,6 +3610,7 @@ "order-priority": "Display order", "key-filter": "Key filter", "key-filters": "Key filters", + "or-conditions-disabled": "OR conditions between key filters are disabled by the system administrator.", "key-name": "Key name", "key-name-required": "Key name is required.", "key-type": { From 2ef626e3d0d20949908475e2495d73dc6743451a Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 11:48:05 +0200 Subject: [PATCH 189/313] Add keyFiltersOperation to Datasource and subscription option interfaces - Add keyFiltersOperation?: ComplexOperation to Datasource interface in widget.models.ts - Add keyFiltersOperation?: ComplexOperation to EntityDataSubscriptionOptions interface - Add keyFiltersOperation?: ComplexOperation to AlarmDataSubscriptionOptions interface - Import ComplexOperation from query.models in all three files --- ui-ngx/src/app/core/api/alarm-data-subscription.ts | 2 ++ ui-ngx/src/app/core/api/entity-data-subscription.ts | 2 ++ ui-ngx/src/app/shared/models/widget.models.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/ui-ngx/src/app/core/api/alarm-data-subscription.ts b/ui-ngx/src/app/core/api/alarm-data-subscription.ts index 16dafd53eb..e854798ee5 100644 --- a/ui-ngx/src/app/core/api/alarm-data-subscription.ts +++ b/ui-ngx/src/app/core/api/alarm-data-subscription.ts @@ -23,6 +23,7 @@ import { DatasourceType } from '@shared/models/widget.models'; import { AlarmData, AlarmDataPageLink, + ComplexOperation, EntityFilter, EntityKey, EntityKeyType, @@ -47,6 +48,7 @@ export interface AlarmDataSubscriptionOptions { pageLink?: AlarmDataPageLink; keyFilters?: Array; additionalKeyFilters?: Array; + keyFiltersOperation?: ComplexOperation; subscriptionTimewindow?: SubscriptionTimewindow; } diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 021c5e81e9..db3ac9e7ef 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -34,6 +34,7 @@ import { import { AlarmFilter, ComparisonTsValue, + ComplexOperation, EntityData, EntityDataPageLink, EntityFilter, @@ -112,6 +113,7 @@ export interface EntityDataSubscriptionOptions { pageLink?: EntityDataPageLink; keyFilters?: Array; additionalKeyFilters?: Array; + keyFiltersOperation?: ComplexOperation; subscriptionTimewindow?: SubscriptionTimewindow; latestTsOffset?: number; } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 1a2f7e1c8b..982f492eba 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -24,6 +24,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { AlarmFilter, AlarmFilterConfig, + ComplexOperation, EntityDataPageLink, EntityFilter, KeyFilter @@ -450,6 +451,7 @@ export interface Datasource { origDatasourceIndex?: number; pageLink?: EntityDataPageLink; keyFilters?: Array; + keyFiltersOperation?: ComplexOperation; entityFilter?: EntityFilter; alarmFilterConfig?: AlarmFilterConfig; alarmFilter?: AlarmFilter; From cea7fc2b69f6a50648295ee9e8808db685407d05 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 11:50:11 +0200 Subject: [PATCH 190/313] Thread keyFiltersOperation through data subscription pipeline - alias-controller: propagate keyFiltersOperation from FilterInfo to Datasource - entity-data.service: propagate keyFiltersOperation from Datasource to EntityDataSubscriptionOptions - entity-data-subscription: include keyFiltersOperation in entity data, entity count, and alarm count query construction - alarm-data-subscription: include keyFiltersOperation in alarm data query construction - alarm-data.service: propagate keyFiltersOperation from alarmSource to AlarmDataSubscriptionOptions --- ui-ngx/src/app/core/api/alarm-data-subscription.ts | 1 + ui-ngx/src/app/core/api/alarm-data.service.ts | 1 + ui-ngx/src/app/core/api/alias-controller.ts | 4 ++++ ui-ngx/src/app/core/api/entity-data-subscription.ts | 7 +++++-- ui-ngx/src/app/core/api/entity-data.service.ts | 1 + 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/core/api/alarm-data-subscription.ts b/ui-ngx/src/app/core/api/alarm-data-subscription.ts index e854798ee5..ad6abb6573 100644 --- a/ui-ngx/src/app/core/api/alarm-data-subscription.ts +++ b/ui-ngx/src/app/core/api/alarm-data-subscription.ts @@ -122,6 +122,7 @@ export class AlarmDataSubscription { entityFilter: this.alarmDataSubscriptionOptions.entityFilter, pageLink: deepClone(this.alarmDataSubscriptionOptions.pageLink), keyFilters, + keyFiltersOperation: this.alarmDataSubscriptionOptions.keyFiltersOperation, alarmFields, entityFields, latestValues diff --git a/ui-ngx/src/app/core/api/alarm-data.service.ts b/ui-ngx/src/app/core/api/alarm-data.service.ts index 901155dec5..c0c5a928bf 100644 --- a/ui-ngx/src/app/core/api/alarm-data.service.ts +++ b/ui-ngx/src/app/core/api/alarm-data.service.ts @@ -85,6 +85,7 @@ export class AlarmDataService { alarmDataSubscriptionOptions.entityFilter = alarmSource.entityFilter; alarmDataSubscriptionOptions.pageLink = pageLink; alarmDataSubscriptionOptions.keyFilters = alarmSource.keyFilters; + alarmDataSubscriptionOptions.keyFiltersOperation = alarmSource.keyFiltersOperation; alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; } return alarmDataSubscriptionOptions; diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index ffd06a5289..d48bcb574b 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -315,6 +315,10 @@ export class AliasController implements IAliasController { || newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.filterId) { newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); + const filterInfo = this.getFilterInfo(newDatasource.filterId); + if (filterInfo?.keyFiltersOperation) { + newDatasource.keyFiltersOperation = filterInfo.keyFiltersOperation; + } } if (newDatasource.type === DatasourceType.alarmCount) { newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false); diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index db3ac9e7ef..e367ae8faa 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -372,6 +372,7 @@ export class EntityDataSubscription { entityFilter: this.entityDataSubscriptionOptions.entityFilter, pageLink: this.entityDataSubscriptionOptions.pageLink, keyFilters, + keyFiltersOperation: this.entityDataSubscriptionOptions.keyFiltersOperation, entityFields, latestValues: this.latestValues }; @@ -496,7 +497,8 @@ export class EntityDataSubscription { } this.countCommand.query = { entityFilter: this.entityDataSubscriptionOptions.entityFilter, - keyFilters + keyFilters, + keyFiltersOperation: this.entityDataSubscriptionOptions.keyFiltersOperation }; this.subscriber.subscriptionCommands.push(this.countCommand); @@ -571,7 +573,8 @@ export class EntityDataSubscription { } this.alarmCountCommand.query = { entityFilter: this.entityDataSubscriptionOptions.entityFilter, - keyFilters + keyFilters, + keyFiltersOperation: this.entityDataSubscriptionOptions.keyFiltersOperation }; if (this.entityDataSubscriptionOptions.alarmFilter) { this.alarmCountCommand.query = {...this.alarmCountCommand.query, ...this.entityDataSubscriptionOptions.alarmFilter}; diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index 9ed064403c..697187b61e 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -183,6 +183,7 @@ export class EntityDataService { entityDataSubscriptionOptions.alarmFilter = datasource.alarmFilter; entityDataSubscriptionOptions.keyFilters = keyFilters; entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; + entityDataSubscriptionOptions.keyFiltersOperation = datasource.keyFiltersOperation; if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { entityDataSubscriptionOptions.pageLink = pageLink; } From 38833727dca7dc6bbccaeb28dece8109fe181270 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 12:02:56 +0200 Subject: [PATCH 191/313] Add allowKeyFiltersOrConditions to emptyUserAuthState initializer --- ui-ngx/src/app/core/auth/auth.reducer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 51e02b1ab8..669c47fd78 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -42,7 +42,8 @@ const emptyUserAuthState: AuthPayload = { maxDebugModeDurationMinutes: 0, intermediateAggregationIntervalInSecForCF: 0, userSettings: initialUserSettings, - trendzSettings: initialTrendzSettings + trendzSettings: initialTrendzSettings, + allowKeyFiltersOrConditions: false }; export const initialState: AuthState = { From 2093da73e1ba8264f2fecfd41de6a9e7a342abc9 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 12:21:35 +0200 Subject: [PATCH 192/313] Fix filter preview reactivity and keyFiltersOperation change detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FilterTextComponent: implement OnChanges to re-render preview text when the operation input changes (AND→OR or OR→AND) - isFilterEqual: include keyFiltersOperation in comparison so changing only the operation triggers widget subscription refresh --- .../home/components/filter/filter-text.component.ts | 12 ++++++++++-- ui-ngx/src/app/shared/models/query/query.models.ts | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts index 94ef717912..75713c77e3 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { ControlValueAccessor, UntypedFormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { ComplexOperation, KeyFilter, keyFiltersToText } from '@shared/models/query/query.models'; @@ -35,7 +35,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; ], standalone: false }) -export class FilterTextComponent implements ControlValueAccessor, OnInit { +export class FilterTextComponent implements ControlValueAccessor, OnInit, OnChanges { private requiredValue: boolean; get required(): boolean { @@ -65,6 +65,7 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit { public filterText: string; + private currentValue: Array; private propagateChange = (v: any) => { }; constructor(private dialog: MatDialog, @@ -83,11 +84,18 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit { ngOnInit() { } + ngOnChanges(changes: SimpleChanges) { + if (changes.operation && !changes.operation.firstChange) { + this.updateFilterText(this.currentValue); + } + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } writeValue(value: Array): void { + this.currentValue = value; this.updateFilterText(value); } diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 6273fbd434..258f09a535 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -950,7 +950,8 @@ export const getFilterId = (filters: Filters, filterInfo: FilterInfo): string => } const isFilterEqual = (filter1: FilterInfo, filter2: FilterInfo): boolean => { - return isEqual(filter1.keyFilters, filter2.keyFilters); + return isEqual(filter1.keyFilters, filter2.keyFilters) && + filter1.keyFiltersOperation === filter2.keyFiltersOperation; } const createFilterName = (filters: Filters, filter: string): string => { From 47f26f136af5098cbe7c92e913b60d0232291905 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 12:55:39 +0200 Subject: [PATCH 193/313] Fix keyFiltersOperation not persisted in dashboard configuration filters-dialog.component.ts was the bridge between filter-dialog (which sets keyFiltersOperation on the Filter object) and the dashboard configuration JSON. It was dropping the field in three places: - createFilterFormControl: form group had no keyFiltersOperation field - edit path: patched filter/editable/keyFilters but not operation - save(): rebuilt Filter objects without keyFiltersOperation --- .../home/components/filter/filters-dialog.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts index 8282c2cd21..46784b5309 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts @@ -37,7 +37,7 @@ import { TranslateService } from '@ngx-translate/core'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { DialogService } from '@core/services/dialog.service'; import { deepClone, isUndefined } from '@core/utils'; -import { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models'; +import { ComplexOperation, Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; @@ -132,7 +132,8 @@ export class FiltersDialogComponent extends DialogComponent = filterValue.keyFilters; const editable: boolean = filterValue.editable; + const keyFiltersOperation: ComplexOperation = filterValue.keyFiltersOperation; if (uniqueFilterList[filter]) { valid = false; message = this.translate.instant('filter.duplicate-filter-error', {filter}); @@ -263,7 +266,7 @@ export class FiltersDialogComponent extends DialogComponent Date: Tue, 24 Mar 2026 13:24:22 +0200 Subject: [PATCH 194/313] fixed json property name for ShortCustomerInfo.publicCustomer --- .../thingsboard/server/common/data/ShortCustomerInfo.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java index dbed1026a2..f1de65f1f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java @@ -40,14 +40,14 @@ public class ShortCustomerInfo { @NoXss private String title; - @JsonProperty("isPublic") + @JsonProperty("public") @Schema(description = "Indicates special 'Public' customer used to embed dashboards on public websites.") private boolean publicCustomer; - @JsonProperty("isPublic") + @JsonProperty("public") public boolean isPublic() { return publicCustomer; } - @JsonProperty("isPublic") + @JsonProperty("public") public void setPublic(boolean publicCustomer) { this.publicCustomer = publicCustomer; } @Override From d1566449d0c22d7d2344a8820d35b336b63b876d Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 13:32:22 +0200 Subject: [PATCH 195/313] Move OR validation to BaseEntityService and fix toggle behavior - Move validateKeyFiltersOperation from DefaultEntityQueryService to BaseEntityService.validateEntityCountQuery so both REST and WebSocket paths are covered - Fix AND/OR toggle: use valueChanges listener to prevent OR selection when disabled (instead of [disabled] which broke the control entirely) - Force AND when OR is disabled and filter had OR saved - Update test to set flag on BaseEntityService --- .../query/DefaultEntityQueryService.java | 21 ++++++------------- .../controller/EntityQueryControllerTest.java | 7 +++++-- .../server/dao/entity/BaseEntityService.java | 14 +++++++++++-- .../filter/filter-dialog.component.html | 1 - .../filter/filter-dialog.component.ts | 11 +++++++++- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 8da82a54d0..7758fe56e5 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -18,7 +18,7 @@ package org.thingsboard.server.service.query; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.Setter; + import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -45,7 +45,6 @@ import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeySample; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; -import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; @@ -95,10 +94,6 @@ public class DefaultEntityQueryService implements EntityQueryService { @Value("${server.ws.max_entities_per_alarm_subscription:1000}") private int maxEntitiesPerAlarmSubscription; - @Setter - @Value("${sql.query.key-filters-or-conditions.enabled:true}") - private boolean keyFiltersOrConditionsEnabled; - @Autowired private DbCallbackExecutorService dbCallbackExecutor; @@ -110,13 +105,13 @@ public class DefaultEntityQueryService implements EntityQueryService { @Override public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) { - validateKeyFiltersOperation(query); + return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); } @Override public PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) { - validateKeyFiltersOperation(query); + if (query.getKeyFilters() != null) { resolveDynamicValuesInPredicates( query.getKeyFilters().stream() @@ -186,7 +181,7 @@ public class DefaultEntityQueryService implements EntityQueryService { @Override public PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) { - validateKeyFiltersOperation(query); + EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); PageData entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), entityDataQuery); @@ -213,7 +208,7 @@ public class DefaultEntityQueryService implements EntityQueryService { @Override public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) { - validateKeyFiltersOperation(query); + if (query.getEntityFilter() != null) { EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); PageData entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), @@ -246,11 +241,7 @@ public class DefaultEntityQueryService implements EntityQueryService { return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } - private void validateKeyFiltersOperation(EntityCountQuery query) { - if (!keyFiltersOrConditionsEnabled && query.getKeyFiltersOperation() == ComplexOperation.OR) { - throw new IllegalArgumentException("OR conditions between key filters are disabled"); - } - } + @Override public ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index bf9762b208..d8081f60dd 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -90,6 +90,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.edqs.util.EdqsRocksDb; @@ -127,6 +128,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest { private QueueStatsService queueStatsService; @Autowired private DefaultEntityQueryService entityQueryService; + @Autowired + private BaseEntityService baseEntityService; @MockitoBean private EdqsRocksDb edqsRocksDb; @@ -2495,7 +2498,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { public void testOrKeyFiltersOperationRejectedWhenDisabled() throws Exception { loginTenantAdmin(); - entityQueryService.setKeyFiltersOrConditionsEnabled(false); + baseEntityService.setKeyFiltersOrConditionsEnabled(false); try { DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("default")); @@ -2516,7 +2519,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityCountQuery explicitAndQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.AND); doPost("/api/entitiesQuery/count", explicitAndQuery).andExpect(status().isOk()); } finally { - entityQueryService.setKeyFiltersOrConditionsEnabled(true); + baseEntityService.setKeyFiltersOrConditionsEnabled(true); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index e1b3b084ec..98a1113157 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -19,8 +19,10 @@ import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -40,6 +42,7 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; @@ -91,6 +94,10 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe private static final Set EXCLUDED_TYPES_FROM_OPTIMIZATION = Set.of( EntityFilterType.ENTITY_LIST, EntityFilterType.SINGLE_ENTITY, EntityFilterType.RELATIONS_QUERY); + @Setter + @Value("${sql.query.key-filters-or-conditions.enabled:true}") + private boolean keyFiltersOrConditionsEnabled; + @Autowired private EntityQueryDao entityQueryDao; @@ -328,7 +335,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return new NameLabelAndCustomerDetails(getName(entity), getLabel(entity), getCustomerId(entity)); } - private static void validateEntityCountQuery(EntityCountQuery query) { + private void validateEntityCountQuery(EntityCountQuery query) { if (query == null) { throw new IncorrectParameterException("Query must be specified."); } else if (query.getEntityFilter() == null) { @@ -342,9 +349,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } else if (query.getEntityFilter().getType().equals(ENTITY_NAME)) { validateEntityNameQuery((EntityNameFilter) query.getEntityFilter()); } + if (!keyFiltersOrConditionsEnabled && query.getKeyFiltersOperation() == ComplexOperation.OR) { + throw new IncorrectParameterException("OR conditions between key filters are disabled"); + } } - private static void validateEntityDataQuery(EntityDataQuery query) { + private void validateEntityDataQuery(EntityDataQuery query) { validateEntityCountQuery(query); validateEntityDataPageLink(query.getPageLink()); } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html index 581dd42c93..6865bfd9c3 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html @@ -51,7 +51,6 @@
filter.key-filters
{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts index 465b87b4b1..b77a951984 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts @@ -94,12 +94,21 @@ export class FilterDialogComponent extends DialogComponent { + if (value === ComplexOperation.OR) { + this.filterFormGroup.get('keyFiltersOperation').setValue(ComplexOperation.AND, {emitEvent: false}); + } + }); + } } validateDuplicateFilterName(): ValidatorFn { From 4e964b213305c132db764fe5cac3900917957fc0 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 24 Mar 2026 13:04:03 +0100 Subject: [PATCH 196/313] Moved templates Control flow syntax to new version --- .../ai-model/ai-model-dialog.component.html | 207 +++-- .../check-connectivity-dialog.component.html | 40 +- .../alarm-rule-dialog.component.html | 8 +- .../alarm-rule-filter-config.component.html | 104 ++- .../alarm-rules/alarm-rules.component.html | 60 +- ...alarm-rule-condition-dialog.component.html | 26 +- .../cf-alarm-schedule.component.html | 56 +- .../create-cf-alarm-rules.component.html | 32 +- .../alarm-rule-filter-dialog.component.html | 18 +- ...ter-predicate-no-data-value.component.html | 26 +- ...alarm-rule-filter-predicate.component.html | 24 +- .../alarm/alarm-assignee-panel.component.html | 60 +- .../alarm-assignee-select.component.html | 22 +- .../alarm/alarm-assignee.component.html | 13 +- .../alarm/alarm-comment-dialog.component.html | 10 +- .../alarm/alarm-comment.component.html | 206 ++--- .../alarm/alarm-details-dialog.component.html | 89 +- .../alarm/alarm-filter-config.component.html | 132 ++- ...aliases-entity-autocomplete.component.html | 32 +- ...aliases-entity-select-panel.component.html | 20 +- .../aliases-entity-select.component.html | 26 +- .../alias/entity-alias-dialog.component.html | 22 +- .../entity-aliases-dialog.component.html | 90 +- .../api-key/add-api-key-dialog.component.html | 8 +- ...t-api-key-description-panel.component.html | 8 +- .../add-attribute-dialog.component.html | 10 +- ...-widget-to-dashboard-dialog.component.html | 14 +- .../attribute/attribute-table.component.html | 220 ++--- .../delete-timeseries-panel.component.html | 46 +- .../calculated-field.component.html | 180 ++-- ...ntity-aggregation-component.component.html | 12 +- ...eofencing-zone-groups-panel.component.html | 168 ++-- ...ulated-fields-filter-config.component.html | 108 ++- .../add-widget-dialog.component.html | 64 +- .../dashboard-image-dialog.component.html | 21 +- .../dashboard-page.component.html | 764 +++++++++-------- .../dashboard-settings-dialog.component.html | 430 +++++----- .../dashboard-state.component.html | 28 +- .../dashboard-widget-select.component.html | 100 +-- .../dashboard-page/edit-widget.component.html | 81 +- .../add-new-breakpoint-dialog.component.html | 24 +- .../layout/dashboard-layout.component.html | 50 +- ...ge-dashboard-layouts-dialog.component.html | 387 +++++---- .../layout/move-widgets-dialog.component.html | 6 +- ...select-dashboard-breakpoint.component.html | 12 +- .../dashboard-state-dialog.component.html | 30 +- .../default-state-controller.component.html | 10 +- .../entity-state-controller.component.html | 28 +- .../widget-types-panel.component.html | 10 +- .../dashboard/dashboard.component.html | 88 +- ...select-target-layout-dialog.component.html | 8 +- .../select-target-state-dialog.component.html | 14 +- .../components/details-panel.component.html | 70 +- .../device/device-info-filter.component.html | 82 +- .../entity/add-entity-dialog.component.html | 10 +- ...entity-debug-settings-panel.component.html | 18 +- .../entity/entities-table.component.html | 403 +++++---- .../entity/entities-table.component.ts | 22 +- .../entity/entity-chips.component.html | 10 +- .../entity/entity-details-page.component.html | 40 +- .../entity/entity-filter-view.component.html | 7 +- .../entity/entity-filter.component.html | 613 +++++++------- .../event/event-filter-panel.component.html | 44 +- .../event/event-table-header.component.html | 8 +- .../boolean-filter-predicate.component.html | 8 +- ...lex-filter-predicate-dialog.component.html | 21 +- .../filter/filter-dialog.component.html | 22 +- .../filter-predicate-list.component.html | 70 +- .../filter-predicate-value.component.html | 92 +- .../filter/filter-predicate.component.html | 86 +- .../filter-user-info-dialog.component.html | 25 +- .../filter/filters-dialog.component.html | 94 ++- .../filter/filters-edit-panel.component.html | 30 +- .../filter/filters-edit.component.html | 26 +- .../filter/key-filter-dialog.component.html | 143 ++-- .../filter/key-filter-list.component.html | 60 +- .../numeric-filter-predicate.component.html | 8 +- .../string-filter-predicate.component.html | 8 +- .../filter/user-filter-dialog.component.html | 72 +- .../send-notification-button.component.html | 20 +- .../show-notification-popover.component.html | 75 +- .../show-notification-popover.component.ts | 8 +- ...rm-duration-predicate-value.component.html | 56 +- .../alarm/alarm-dynamic-value.component.html | 8 +- ...alarm-rule-condition-dialog.component.html | 83 +- .../alarm/alarm-rule-condition.component.html | 6 +- .../profile/alarm/alarm-rule.component.html | 58 +- .../alarm-schedule-dialog.component.html | 19 +- .../alarm/alarm-schedule.component.html | 175 ++-- .../profile/alarm/alarm-schedule.component.ts | 2 +- .../alarm/create-alarm-rules.component.html | 97 ++- .../alarm/device-profile-alarm.component.html | 131 +-- .../device-profile-alarms.component.html | 43 +- .../edit-alarm-details-dialog.component.html | 20 +- .../asset-profile-autocomplete.component.html | 107 ++- .../asset-profile-dialog.component.html | 10 +- ...device-profile-autocomplete.component.html | 111 ++- .../device-profile-dialog.component.html | 10 +- .../common/time-unit-select.component.html | 32 +- ...evice-profile-configuration.component.html | 14 +- ...ile-transport-configuration.component.html | 56 +- .../lwm2m-attributes-dialog.component.html | 19 +- .../lwm2m-attributes-key-list.component.html | 101 ++- ...ap-add-config-server-dialog.component.html | 20 +- ...object-add-instances-dialog.component.html | 10 +- ...m-object-add-instances-list.component.html | 41 +- .../lwm2m/lwm2m-object-list.component.html | 54 +- ...snmp-device-profile-mapping.component.html | 111 +-- .../tenant-profile-queues.component.html | 76 +- ...tenant-profile-autocomplete.component.html | 99 ++- .../tenant-profile-dialog.component.html | 10 +- .../profile/tenant-profile.component.html | 102 +-- ...enant-profile-configuration.component.html | 704 +++++++++------ .../rate-limits-details-dialog.component.html | 10 +- .../rate-limits-list.component.html | 91 +- .../rate-limits/rate-limits.component.html | 14 +- ...enant-profile-configuration.component.html | 16 +- .../queue/queue-form.component.html | 224 +++-- .../relation/relation-dialog.component.html | 22 +- .../relation/relation-filters.component.html | 62 +- .../relation/relation-table.component.html | 84 +- .../resources/resources-dialog.component.html | 10 +- .../components/router-tabs.component.html | 30 +- .../rule-chain-autocomplete.component.html | 69 +- ...advanced-processing-setting.component.html | 52 +- .../assign-customer-config.component.html | 10 +- .../action/attributes-config.component.html | 40 +- .../action/attributes-config.component.ts | 2 +- .../action/clear-alarm-config.component.html | 84 +- .../action/create-alarm-config.component.html | 233 ++--- .../action/create-alarm-config.component.ts | 2 +- .../create-relation-config.component.html | 67 +- .../create-relation-config.component.ts | 6 +- .../delete-attributes-config.component.html | 40 +- .../delete-attributes-config.component.ts | 2 +- .../delete-relation-config.component.html | 48 +- .../delete-relation-config.component.ts | 6 +- .../action/device-state-config.component.html | 18 +- .../action/generator-config.component.html | 138 +-- .../gps-geo-action-config.component.html | 288 ++++--- .../action/gps-geo-action-config.component.ts | 6 +- .../action/log-config.component.html | 76 +- .../math-function-config.component.html | 112 +-- .../action/msg-count-config.component.html | 24 +- .../action/msg-delay-config.component.html | 63 +- .../push-to-cloud-config.component.html | 8 +- .../action/push-to-cloud-config.component.ts | 2 +- .../action/push-to-edge-config.component.html | 8 +- .../action/push-to-edge-config.component.ts | 2 +- .../action/rpc-request-config.component.html | 16 +- ...save-to-custom-table-config.component.html | 26 +- .../unassign-customer-config.component.html | 22 +- .../arguments-map-config.component.html | 206 +++-- .../common/credentials-config.component.html | 129 +-- ...vice-relations-query-config.component.html | 41 +- .../common/example-hint.component.html | 18 +- .../common/kv-map-config-old.component.html | 64 +- .../common/kv-map-config.component.html | 76 +- .../math-function-autocomplete.component.html | 36 +- .../message-types-config.component.html | 72 +- .../common/msg-metadata-chip.component.html | 10 +- ...t-message-type-autocomplete.component.html | 49 +- .../relations-query-config-old.component.html | 8 +- .../relations-query-config-old.component.ts | 2 +- .../relations-query-config.component.html | 39 +- .../common/select-attributes.component.html | 19 +- .../common/sv-map-config.component.html | 66 +- .../calculate-delta-config.component.html | 62 +- .../customer-attributes-config.component.html | 8 +- ...emetry-from-database-config.component.html | 248 +++--- .../related-attributes-config.component.html | 8 +- .../tenant-attributes-config.component.html | 8 +- .../external/ai-config.component.html | 40 +- .../azure-iot-hub-config.component.html | 157 ++-- .../external/kafka-config.component.html | 76 +- .../external/lambda-config.component.html | 68 +- .../external/mqtt-config.component.html | 64 +- .../external/pubsub-config.component.html | 16 +- .../external/rabbit-mq-config.component.html | 56 +- .../rest-api-call-config.component.html | 164 ++-- .../external/send-email-config.component.html | 196 +++-- .../external/send-sms-config.component.html | 27 +- .../external/slack-config.component.html | 34 +- .../external/sns-config.component.html | 32 +- .../external/sqs-config.component.html | 66 +- .../external/sqs-config.component.ts | 2 +- .../check-relation-config.component.html | 41 +- .../gps-geo-filter-config.component.html | 160 ++-- .../filter/script-config.component.html | 76 +- .../filter/switch-config.component.html | 76 +- .../rule-node/rule-node-config.models.ts | 2 +- .../change-originator-config.component.html | 93 +- .../deduplication-config.component.html | 120 +-- .../deduplication-config.component.ts | 6 +- .../node-json-path-config.component.html | 4 +- .../rename-keys-config.component.html | 8 +- .../script-config.component.html | 76 +- .../to-email-config.component.html | 124 +-- ...-sns-provider-configuration.component.html | 24 +- ...-sms-provider-configuration.component.html | 88 +- ...pp-sms-provider-configuration.component.ts | 8 +- .../sms-provider-configuration.component.html | 56 +- .../sms-provider-configuration.component.ts | 2 +- ...-sms-provider-configuration.component.html | 32 +- .../vc/auto-commit-settings.component.html | 147 ++-- .../vc/entity-version-diff.component.html | 106 +-- .../vc/entity-versions-table.component.html | 96 ++- .../vc/version-control.component.html | 19 +- ...anage-widget-actions-dialog.component.html | 10 +- .../widget-action-dialog.component.html | 167 ++-- .../action-button-basic-config.component.html | 21 +- .../power-button-basic-config.component.html | 22 +- ...gmented-button-basic-config.component.html | 79 +- .../aggregated-data-keys-panel.component.html | 29 +- ...bel-value-card-basic-config.component.html | 10 +- ...le-app-qr-code-basic-config.component.html | 192 +++-- .../progress-bar-basic-config.component.html | 52 +- .../simple-card-basic-config.component.html | 16 +- .../value-card-basic-config.component.html | 24 +- ...lue-chart-card-basic-config.component.html | 14 +- ...rt-with-labels-basic-config.component.html | 54 +- .../comparison-keys-table.component.html | 25 +- .../latest-chart-basic-config.component.html | 134 +-- .../range-chart-basic-config.component.html | 214 ++--- ...e-series-chart-basic-config.component.html | 122 +-- .../basic/common/data-key-row.component.html | 162 ++-- .../common/data-keys-panel.component.html | 121 +-- .../widget-actions-panel.component.html | 30 +- ...entities-table-basic-config.component.html | 12 +- .../analog-gauge-basic-config.component.html | 58 +- .../compass-gauge-basic-config.component.html | 10 +- ...l-simple-gauge-basic-config.component.html | 57 +- .../battery-level-basic-config.component.html | 38 +- ...uid-level-card-basic-config.component.html | 637 +++++++------- ...ignal-strength-basic-config.component.html | 24 +- .../status-widget-basic-config.component.html | 34 +- .../basic/map/map-basic-config.component.html | 10 +- .../single-switch-basic-config.component.html | 22 +- .../rpc/slider-basic-config.component.html | 136 +-- .../value-stepper-basic-config.component.html | 22 +- ...peed-direction-basic-config.component.html | 68 +- .../widget/config/datasource.component.html | 222 ++--- .../widget/config/datasources.component.html | 117 +-- .../config/target-device.component.html | 27 +- .../embed-dashboard-dialog.component.html | 12 +- .../alarm/alarms-table-widget.component.html | 218 ++--- .../command-button-widget.component.html | 4 +- .../toggle-button-widget.component.html | 14 +- .../two-segment-button-widget.component.html | 4 +- ...ggregated-value-card-widget.component.html | 78 +- .../cards/label-card-widget.component.html | 4 +- .../label-value-card-widget.component.html | 16 +- ...ification-type-filter-panel.component.html | 23 +- .../cards/progress-bar-widget.component.html | 30 +- .../unread-notification-widget.component.html | 56 +- .../unread-notification-widget.component.ts | 6 +- .../cards/value-card-widget.component.html | 38 +- .../value-chart-card-widget.component.html | 8 +- ...ar-chart-with-labels-widget.component.html | 24 +- .../lib/chart/latest-chart.component.html | 26 +- .../chart/range-chart-widget.component.html | 20 +- .../time-series-chart-widget.component.html | 170 ++-- .../lib/count/count-widget.component.html | 30 +- .../date-range-navigator-panel.component.html | 24 +- .../date-range-navigator.component.html | 36 +- .../lib/edges-overview-widget.component.html | 8 +- .../entities-table-widget.component.html | 99 ++- .../widget/lib/flot-widget.component.html | 28 +- .../lib/home-page/doc-link.component.html | 65 +- .../home-page/doc-links-widget.component.html | 22 +- .../edit-links-dialog.component.html | 103 +-- .../getting-started-widget.component.html | 332 ++++---- .../lib/home-page/quick-link.component.html | 107 ++- .../quick-links-widget.component.html | 22 +- .../recent-dashboards-widget.component.html | 147 ++-- .../usage-info-widget.component.html | 19 +- .../lib/home-page/version-info.component.html | 37 +- .../battery-level-widget.component.html | 31 +- .../liquid-level-widget.component.html | 14 +- .../signal-strength-widget.component.html | 46 +- .../indicator/status-widget.component.html | 12 +- .../lib/json-input-widget.component.html | 78 +- .../widget/lib/legend.component.html | 156 ++-- .../select-entity-dialog.component.html | 18 +- .../panels/map-timeline-panel.component.html | 8 +- .../select-map-entity-panel.component.html | 8 +- .../mobile-app-qrcode-widget.component.html | 40 +- .../lib/multiple-input-widget.component.html | 477 ++++++----- .../navigation-cards-widget.component.html | 44 +- .../lib/photo-camera-input.component.html | 88 +- .../widget/lib/qrcode-widget.component.html | 8 +- .../rpc/persistent-add-dialog.component.html | 30 +- .../persistent-details-dialog.component.html | 41 +- .../persistent-filter-panel.component.html | 8 +- .../rpc/persistent-filter-panel.component.ts | 2 +- .../lib/rpc/persistent-table.component.html | 70 +- .../rpc/power-button-widget.component.html | 4 +- .../rpc/single-switch-widget.component.html | 34 +- .../lib/rpc/slider-widget.component.html | 56 +- .../widget/lib/rpc/switch.component.html | 55 +- .../rpc/value-stepper-widget.component.html | 6 +- .../scada/scada-symbol-widget.component.html | 12 +- ...larms-table-widget-settings.component.html | 42 +- ...ower-button-widget-settings.component.html | 22 +- ...nted-button-widget-settings.component.html | 62 +- ...ted-value-card-key-settings.component.html | 8 +- .../api-usage-widget-settings.component.html | 55 +- ...board-state-widget-settings.component.html | 22 +- .../cards/label-widget-label.component.html | 24 +- .../label-widget-settings.component.html | 31 +- .../cards/label-widget-settings.component.ts | 3 + ...app-qr-code-widget-settings.component.html | 78 +- ...rogress-bar-widget-settings.component.html | 42 +- .../qrcode-widget-settings.component.html | 8 +- ...eries-table-widget-settings.component.html | 76 +- .../value-card-widget-settings.component.html | 14 +- ...-chart-card-widget-settings.component.html | 14 +- ...with-labels-widget-settings.component.html | 58 +- .../chart/flot-key-settings.component.html | 21 +- .../flot-pie-widget-settings.component.html | 30 +- .../chart/flot-threshold.component.html | 16 +- .../chart/flot-widget-settings.component.html | 347 ++++---- .../chart/flot-widget-settings.component.ts | 4 + .../chart/label-data-key.component.html | 24 +- ...atest-chart-widget-settings.component.html | 84 +- ...range-chart-widget-settings.component.html | 86 +- ...e-series-chart-key-settings.component.html | 99 ++- ...-series-chart-line-settings.component.html | 156 +++- ...eries-chart-widget-settings.component.html | 54 +- ...custom-action-pretty-editor.component.html | 53 +- ...ction-pretty-resources-tabs.component.html | 78 +- ...value-action-settings-panel.component.html | 251 +++--- .../mobile-action-editor.component.html | 35 +- .../action/mobile-action-editor.component.ts | 2 +- ...value-action-settings-panel.component.html | 215 ++--- .../action/widget-action.component.html | 493 ++++++----- .../common/advanced-range.component.html | 18 +- .../alias/entity-alias-select.component.html | 114 +-- ...-date-format-settings-panel.component.html | 22 +- .../common/axis-scale-row.component.html | 89 +- .../background-settings-panel.component.html | 12 +- .../widget-button-appearance.component.html | 58 +- ...t-button-custom-style-panel.component.html | 24 +- .../widget-button-custom-style.component.html | 18 +- ...n-toggle-custom-style-panel.component.html | 24 +- ...-button-toggle-custom-style.component.html | 18 +- .../chart-animation-settings.component.html | 16 +- .../chart/chart-bar-settings.component.html | 24 +- .../chart/chart-fill-settings.component.html | 42 +- ...-series-chart-axis-settings.component.html | 182 ++-- ...time-series-chart-state-row.component.html | 33 +- ...e-series-chart-states-panel.component.html | 27 +- ...-series-chart-threshold-row.component.html | 138 +-- ...rt-threshold-settings-panel.component.html | 56 +- ...es-chart-threshold-settings.component.html | 27 +- ...ries-chart-thresholds-panel.component.html | 37 +- ...e-series-chart-y-axes-panel.component.html | 42 +- ...ime-series-chart-y-axis-row.component.html | 26 +- ...regation-bar-width-settings.component.html | 40 +- .../common/color-range-list.component.html | 164 ++-- .../common/color-range-panel.component.html | 58 +- .../color-settings-panel.component.html | 34 +- .../common/color-settings.component.html | 13 +- .../count-widget-settings.component.html | 34 +- .../common/css-size-input.component.html | 17 +- .../common/css-unit-select.component.html | 10 +- .../common/date-format-select.component.html | 25 +- .../dynamic-form-array.component.html | 95 ++- .../dynamic-form-properties.component.html | 133 +-- ...dynamic-form-property-panel.component.html | 798 ++++++++++-------- .../dynamic-form-property-row.component.html | 26 +- ...ynamic-form-select-item-row.component.html | 18 +- .../dynamic-form-select-items.component.html | 76 +- .../dynamic-form/dynamic-form.component.html | 520 +++++++----- .../common/entity-alias-input.component.html | 41 +- .../filter/filter-select.component.html | 96 ++- .../common/font-settings-panel.component.html | 96 ++- .../settings/common/gradient.component.html | 336 ++++---- .../common/image-cards-select.component.html | 18 +- ...tatus-widget-state-settings.component.html | 102 ++- .../key/data-key-config-dialog.component.html | 58 +- .../common/key/data-key-config.component.html | 363 ++++---- .../common/legend-config.component.html | 32 +- ...itional-map-data-source-row.component.html | 57 +- ...additional-map-data-sources.component.html | 25 +- .../data-layer-color-settings.component.html | 13 +- ...data-layer-pattern-settings.component.html | 84 +- .../image-map-source-settings.component.html | 14 +- .../map/map-action-button-row.component.html | 17 +- .../map/map-data-layer-dialog.component.html | 502 ++++++----- .../map/map-data-layer-row.component.html | 230 ++--- .../common/map/map-data-layers.component.html | 87 +- .../map/map-data-source-row.component.html | 46 +- .../map/map-data-sources.component.html | 25 +- .../common/map/map-layer-row.component.html | 98 ++- .../map-layer-settings-panel.component.html | 124 +-- .../common/map/map-layers.component.html | 51 +- .../common/map/map-settings.component.html | 92 +- .../map-tooltip-tag-actions.component.html | 90 +- .../map/marker-icon-shapes.component.html | 6 +- .../map/marker-image-settings.component.html | 9 +- .../map/marker-shape-settings.component.html | 12 +- .../common/map/marker-shapes.component.html | 36 +- .../shape-fill-image-settings.component.html | 11 +- .../map/trip-timeline-settings.component.html | 10 +- ...cada-symbol-object-settings.component.html | 110 +-- .../value-source-data-key.component.html | 108 +-- .../common/value-source.component.html | 44 +- .../common/widget-font.component.html | 15 +- .../widget/widget-settings.component.html | 14 +- .../device-key-autocomplete.component.html | 57 +- ...stent-table-widget-settings.component.html | 83 +- ...ngle-switch-widget-settings.component.html | 40 +- .../slider-widget-settings.component.html | 134 +-- ...lue-stepper-widget-settings.component.html | 22 +- ...ities-table-widget-settings.component.html | 58 +- ...eway-events-widget-settings.component.html | 19 +- ...logue-gauge-widget-settings.component.html | 12 +- ...gital-gauge-widget-settings.component.html | 62 +- .../settings/gauge/tick-value.component.html | 18 +- ...pio-control-widget-settings.component.html | 25 +- .../gpio-control-widget-settings.component.ts | 4 + .../settings/gpio/gpio-item.component.html | 39 +- .../gpio-panel-widget-settings.component.html | 27 +- .../gpio-panel-widget-settings.component.ts | 4 + ...ttery-level-widget-settings.component.html | 28 +- ...-level-card-widget-settings.component.html | 517 ++++++------ ...al-strength-widget-settings.component.html | 14 +- .../status-widget-settings.component.html | 32 +- .../datakey-select-option.component.html | 20 +- ...-attribute-general-settings.component.html | 26 +- ...ple-attributes-key-settings.component.html | 88 +- ...tiple-attributes-key-settings.component.ts | 4 + .../map/legacy/circle-settings.component.html | 8 +- ...atasources-key-autocomplete.component.html | 22 +- ...oogle-map-provider-settings.component.html | 8 +- .../here-map-provider-settings.component.html | 33 +- ...image-map-provider-settings.component.html | 46 +- .../map-provider-settings.component.html | 8 +- .../legacy/map-settings-legacy.component.html | 20 +- .../legacy/markers-settings.component.html | 12 +- ...treet-map-provider-settings.component.html | 8 +- .../legacy/polygon-settings.component.html | 8 +- ...ncent-map-provider-settings.component.html | 8 +- ...rip-animation-path-settings.component.html | 8 +- ...ation-cards-widget-settings.component.html | 20 +- ...d-direction-widget-settings.component.html | 14 +- .../timeseries-table-widget.component.html | 180 ++-- .../trip-animation.component.html | 60 +- .../widget/widget-config.component.html | 604 +++++++------ .../widget/widget-container.component.html | 275 +++--- .../widget/widget-container.component.ts | 48 +- .../components/widget/widget.component.html | 14 +- ...entities-to-customer-dialog.component.html | 12 +- ...add-entities-to-edge-dialog.component.html | 10 +- .../assign-to-customer-dialog.component.html | 10 +- .../src/app/modules/home/home.component.html | 26 +- .../home/menu/menu-link.component.html | 4 +- .../home/menu/menu-toggle.component.html | 14 +- .../home/menu/side-menu.component.html | 16 +- .../entity/entities-table-config.models.ts | 3 + .../entity/entity-table-component.models.ts | 3 +- .../auto-commit-admin-settings.component.html | 11 +- .../admin/general-settings.component.html | 58 +- .../pages/admin/home-settings.component.html | 10 +- .../pages/admin/mail-server.component.html | 489 ++++++----- .../clients/client-dialog.component.html | 10 +- .../oauth2/clients/client.component.html | 548 ++++++------ .../oauth2/domains/domain.component.html | 24 +- .../js-library-table-header.component.html | 8 +- .../resource-library-tabs.component.html | 14 +- .../resource/resource-tabs.component.html | 24 +- .../resources-table-header.component.html | 8 +- .../admin/security-settings.component.html | 262 +++--- .../admin/send-test-sms-dialog.component.html | 26 +- .../pages/admin/sms-provider.component.html | 112 +-- .../admin/trendz-settings.component.html | 34 +- .../two-factor-auth-settings.component.html | 214 +++-- .../dashboard/dashboard-tabs.component.html | 24 +- ...mport-dashboard-file-dialog.component.html | 28 +- ...ake-dashboard-public-dialog.component.html | 14 +- ...-dashboard-customers-dialog.component.html | 10 +- .../device-profile-tabs.component.html | 92 +- .../data/device-configuration.component.html | 16 +- .../device/data/device-data.component.html | 48 +- ...ice-transport-configuration.component.html | 64 +- ...ice-transport-configuration.component.html | 228 ++--- ...e-check-connectivity-dialog.component.html | 503 +++++------ .../device-credentials-dialog.component.html | 39 +- .../edge-instructions-dialog.component.html | 101 +-- .../home/pages/edge/edge-tabs.component.html | 92 +- .../entity-view-tabs.component.html | 82 +- .../home-links/home-links.component.html | 45 +- .../mobile-app-dialog.component.html | 10 +- .../applications/mobile-app.component.html | 193 +++-- .../remove-app-dialog.component.html | 10 +- .../layout/custom-mobile-page.component.html | 87 +- .../default-mobile-page-panel.component.html | 56 +- .../mobile/common/editor-panel.component.html | 17 +- ...ile-qr-code-widget-settings.component.html | 121 +-- .../sent/sent-error-dialog.component.html | 30 +- .../sent/sent-error-dialog.component.ts | 3 +- .../sent-notification-dialog.component.html | 98 ++- .../notification-setting-form.component.html | 16 +- .../notification-settings.component.html | 50 +- .../ota-update/ota-update-tabs.component.html | 14 +- .../home/pages/profile/profile.component.html | 56 +- .../add-rule-node-dialog.component.html | 6 +- .../add-rule-node-link-dialog.component.html | 6 +- ...ate-nested-rulechain-dialog.component.html | 22 +- .../rulechain/link-labels.component.html | 60 +- .../rulechain/rule-node-config.component.html | 20 +- .../rule-node-details.component.html | 16 +- .../rulechain/rulechain-page.component.html | 260 +++--- .../rulechain/rulechain-tabs.component.html | 100 ++- .../pages/rulechain/rulenode.component.html | 86 +- ...scada-symbol-behavior-panel.component.html | 82 +- .../scada-symbol-behavior-row.component.html | 26 +- .../scada-symbol-behaviors.component.html | 80 +- ...metadata-tag-function-panel.component.html | 38 +- .../scada-symbol-metadata-tag.component.html | 36 +- .../scada-symbol-metadata-tags.component.html | 29 +- .../scada-symbol-editor.component.html | 18 +- .../scada-symbol-tooltip.components.ts | 128 +-- .../scada-symbol/scada-symbol.component.html | 158 ++-- .../backup-code-auth-dialog.component.html | 18 +- .../email-auth-dialog.component.html | 34 +- .../sms-auth-dialog.component.html | 18 +- .../totp-auth-dialog.component.html | 18 +- .../pages/security/security.component.html | 239 +++--- .../tenant-profile-tabs.component.html | 10 +- .../home/pages/tenant/tenant.component.html | 46 +- .../activation-link-dialog.component.html | 10 +- .../pages/user/add-user-dialog.component.html | 18 +- .../home/pages/user/user-tabs.component.html | 68 +- .../home/pages/user/user.component.html | 95 ++- .../save-widget-type-as-dialog.component.html | 18 +- .../select-widget-type-dialog.component.html | 28 +- .../pages/widget/widget-editor.component.html | 132 +-- .../widget-type-autocomplete.component.html | 63 +- .../widget/widget-type-tabs.component.html | 14 +- .../widgets-bundle-dialog.component.html | 10 +- .../widget/widgets-bundle-tabs.component.html | 14 +- .../widgets-bundle-widgets.component.html | 202 +++-- .../widget/widgets-bundle.component.html | 70 +- .../login/create-password.component.html | 32 +- ...force-two-factor-auth-login.component.html | 24 +- .../login/pages/login/login.component.html | 20 +- .../reset-password-request.component.html | 18 +- .../pages/login/reset-password.component.html | 40 +- .../two-factor-auth-login.component.html | 135 +-- .../button/copy-button.component.html | 11 +- .../button/toggle-password.component.html | 8 +- .../widget-button-toggle.component.html | 28 +- .../button/widget-button.component.html | 14 +- .../shared/components/cheatsheet.component.ts | 28 +- .../components/color-input.component.html | 53 +- .../color-picker-panel.component.html | 32 +- .../color-picker/color-picker.component.html | 16 +- .../country-autocomplete.component.html | 71 +- .../app/shared/components/css.component.html | 8 +- .../dashboard-autocomplete.component.html | 106 ++- .../dashboard-select-panel.component.html | 8 +- .../dashboard-select.component.html | 8 +- ...ashboard-state-autocomplete.component.html | 30 +- .../dialog/alert-dialog.component.html | 7 +- .../entity-conflict-dialog.component.ts | 6 +- .../dialog/error-alert-dialog.component.html | 10 +- .../entity/entity-autocomplete.component.html | 105 ++- .../entity-gateway-select.component.html | 59 +- .../entity/entity-keys-list.component.html | 25 +- .../entity/entity-list-select.component.html | 48 +- .../entity/entity-list.component.html | 91 +- .../entity/entity-select.component.html | 50 +- ...entity-subtype-autocomplete.component.html | 38 +- .../entity/entity-subtype-list.component.html | 39 +- .../entity-subtype-select.component.html | 20 +- .../entity/entity-type-list.component.html | 47 +- .../entity/entity-type-select.component.html | 20 +- .../entity/entity-type-select.component.ts | 6 +- .../components/file-input.component.html | 30 +- .../footer-fab-buttons.component.html | 18 +- .../grid/scroll-grid.component.html | 35 +- .../components/help-popup.component.html | 88 +- .../hint-tooltip-icon.component.html | 9 +- .../app/shared/components/html.component.html | 8 +- .../components/image-input.component.html | 99 ++- .../image/embed-image-dialog.component.html | 60 +- .../image/gallery-image-input.component.html | 134 +-- .../image/image-dialog.component.html | 78 +- .../image/image-gallery.component.html | 478 ++++++----- .../image/image-references.component.html | 53 +- ...ultiple-gallery-image-input.component.html | 169 ++-- .../image/scada-symbol-input.component.html | 88 +- .../image/upload-image-dialog.component.html | 28 +- .../js-func-module-row.component.html | 21 +- .../components/js-func-modules.component.html | 31 +- .../shared/components/js-func.component.html | 54 +- .../components/json-content.component.html | 48 +- .../json-object-edit.component.html | 20 +- .../json-object-view.component.html | 4 +- .../shared/components/kv-map.component.html | 46 +- .../components/markdown-editor.component.html | 16 +- .../shared/components/markdown.component.html | 8 +- .../material-icon-select.component.html | 33 +- .../components/material-icons.component.html | 102 ++- .../message-type-autocomplete.component.html | 30 +- .../multiple-image-input.component.html | 88 +- .../notification/notification.component.html | 57 +- .../template-autocomplete.component.html | 102 ++- .../template-autocomplete.component.ts | 5 + .../ota-package-autocomplete.component.html | 71 +- .../components/phone-input.component.html | 44 +- .../shared/components/popover.component.ts | 34 +- .../protobuf-content.component.html | 10 +- .../queue/queue-autocomplete.component.html | 63 +- .../relation-type-autocomplete.component.html | 44 +- .../resource-autocomplete.component.html | 62 +- .../resources-in-use-dialog.component.html | 38 +- .../rule-chain-select-panel.component.html | 32 +- ...k-conversation-autocomplete.component.html | 49 +- .../components/snack-bar-component.html | 7 +- .../string-autocomplete.component.html | 54 +- .../string-items-list.component.html | 34 +- ...string-pattern-autocomplete.component.html | 30 +- .../shared/components/svg-xml.component.html | 4 +- .../components/time-unit-input.component.html | 29 +- ...gation-options-config-panel.component.html | 8 +- .../aggregation-type-select.component.html | 12 +- .../grouping-interval-options.component.html | 24 +- .../components/time/datetime.component.html | 18 +- .../history-selector.component.html | 78 +- ...terval-options-config-panel.component.html | 12 +- .../time/quick-time-interval.component.html | 12 +- .../time/timeinterval.component.html | 12 +- .../timewindow-config-dialog.component.html | 343 ++++---- .../time/timewindow-panel.component.html | 358 ++++---- .../components/time/timewindow.component.html | 92 +- .../time/timezone-select.component.html | 46 +- .../components/time/timezone.component.html | 82 +- .../components/toggle-header.component.html | 78 +- .../components/unit-input.component.html | 50 +- .../unit-settings-panel.component.html | 44 +- .../components/user-menu.component.html | 40 +- .../components/value-input.component.html | 172 ++-- .../vc/branch-autocomplete.component.html | 42 +- .../widgets-bundle-search.component.html | 8 +- .../widgets-bundle-select.component.html | 35 +- .../export-resource-dialog.component.html | 6 +- .../import-dialog-csv.component.html | 79 +- .../import-dialog.component.html | 56 +- .../table-columns-assignment.component.html | 32 +- ui-ngx/yarn.lock | 12 - 653 files changed, 24918 insertions(+), 19220 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 4aafb9b441..7c4d8a96a5 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+@if (isLoading$ | async) { + + +} +@if (!(isLoading$ | async)) { +
+}
@@ -35,12 +39,16 @@ ai-models.name - - {{ 'ai-models.name-required' | translate }} - - - {{ 'ai-models.name-max-length' | translate }} - + @if (aiModelForms.get('name').hasError('required') || aiModelForms.get('name').hasError('pattern')) { + + {{ 'ai-models.name-required' | translate }} + + } + @if (aiModelForms.get('name').hasError('maxlength')) { + + {{ 'ai-models.name-max-length' | translate }} + + }
@@ -50,9 +58,11 @@ ai-models.ai-provider - - {{providerTranslationMap.get(provider) | translate}} - + @for (provider of providerMap; track provider) { + + {{providerTranslationMap.get(provider) | translate}} + + }
@@ -61,30 +71,36 @@ ai-models.personal-access-token - - {{ 'ai-models.personal-access-token-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('required') || + aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('pattern')) { + + {{ 'ai-models.personal-access-token-required' | translate }} + + } } @if (providerFieldsList.includes('projectId')) { ai-models.project-id - - {{ 'ai-models.project-id-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.projectId').hasError('required') || + aiModelForms.get('configuration.providerConfig.projectId').hasError('pattern')) { + + {{ 'ai-models.project-id-required' | translate }} + + } } @if (providerFieldsList.includes('location')) { ai-models.location - - {{ 'ai-models.location-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.location').hasError('required') || + aiModelForms.get('configuration.providerConfig.location').hasError('pattern')) { + + {{ 'ai-models.location-required' | translate }} + + } } @if (providerFieldsList.includes('serviceAccountKey')) { @@ -104,10 +120,12 @@ ai-models.endpoint - - {{ 'ai-models.endpoint-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.endpoint').hasError('required') || + aiModelForms.get('configuration.providerConfig.endpoint').hasError('pattern')) { + + {{ 'ai-models.endpoint-required' | translate }} + + } } @if (providerFieldsList.includes('serviceVersion')) { @@ -120,10 +138,12 @@ ai-models.baseurl - - {{ 'ai-models.baseurl-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.baseUrl').hasError('required') || + aiModelForms.get('configuration.providerConfig.baseUrl').hasError('pattern')) { + + {{ 'ai-models.baseurl-required' | translate }} + + } } @if (providerFieldsList.includes('apiKey')) { @@ -131,30 +151,36 @@ ai-models.api-key - - {{ ( provider === aiProvider.OPENAI ? 'ai-models.api-key-open-ai-required' : 'ai-models.api-key-required') | translate }} - + @if (aiModelForms.get('configuration.providerConfig.apiKey').hasError('required') || + aiModelForms.get('configuration.providerConfig.apiKey').hasError('pattern')) { + + {{ ( provider === aiProvider.OPENAI ? 'ai-models.api-key-open-ai-required' : 'ai-models.api-key-required') | translate }} + + } } @if (providerFieldsList.includes('region')) { ai-models.region - - {{ 'ai-models.region-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.region').hasError('required') || + aiModelForms.get('configuration.providerConfig.region').hasError('pattern')) { + + {{ 'ai-models.region-required' | translate }} + + } } @if (providerFieldsList.includes('accessKeyId')) { ai-models.access-key-id - - {{ 'ai-models.access-key-id-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('required') || + aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('pattern')) { + + {{ 'ai-models.access-key-id-required' | translate }} + + } } @if (providerFieldsList.includes('secretAccessKey')) { @@ -162,10 +188,12 @@ ai-models.secret-access-key - - {{ 'ai-models.secret-access-key-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('required') || + aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('pattern')) { + + {{ 'ai-models.secret-access-key-required' | translate }} + + } } @if (provider === aiProvider.OLLAMA) { @@ -185,19 +213,23 @@ ai-models.username - - {{ 'ai-models.username-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.auth.username').hasError('required') || + aiModelForms.get('configuration.providerConfig.auth.username').hasError('pattern')) { + + {{ 'ai-models.username-required' | translate }} + + } ai-models.password - - {{ 'ai-models.password-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.auth.password').hasError('required') || + aiModelForms.get('configuration.providerConfig.auth.password').hasError('pattern')) { + + {{ 'ai-models.password-required' | translate }} + + } } @if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.TOKEN) { @@ -205,10 +237,12 @@ ai-models.token - - {{ 'ai-models.token-required' | translate }} - + @if (aiModelForms.get('configuration.providerConfig.auth.token').hasError('required') || + aiModelForms.get('configuration.providerConfig.auth.token').hasError('pattern')) { + + {{ 'ai-models.token-required' | translate }} + + } }
@@ -240,14 +274,15 @@ - - warning - + @if (aiModelForms.get('configuration').get('temperature').hasError('min')) { + + warning + + }
} @@ -259,15 +294,16 @@ - - warning - + @if (aiModelForms.get('configuration').get('topP').hasError('min') || + aiModelForms.get('configuration').get('topP').hasError('max')) { + + warning + + }
} @@ -279,14 +315,15 @@ - - warning - + @if (aiModelForms.get('configuration').get('topK').hasError('min')) { + + warning + + }
} diff --git a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html index 8655789594..28d0fa3904 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html @@ -26,25 +26,31 @@
- - -
-
- check_circle + @if (!showCheckSuccess && !checkErrMsg) { + + + } + @if (showCheckSuccess) { +
+
+ check_circle +
+ {{ "ai-models.check-connectivity-success" | translate }}
- {{ "ai-models.check-connectivity-success" | translate }} -
-
-
- cancel + } + @if (checkErrMsg) { +
+
+ cancel +
+
{{ "ai-models.check-connectivity-failed" | translate }}
+ +
-
{{ "ai-models.check-connectivity-failed" | translate }}
- - -
+ }
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 27c7ecf63e..48f4e8a5af 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 @@ -102,9 +102,11 @@ delete
-
- alarm-rule.no-clear-alarm-rule -
+ @if (!configFormGroup.get('clearRule').value) { +
+ alarm-rule.no-clear-alarm-rule +
+ }
+ } @else { +
+
+
alarm-rule.alarm-rule-type-list
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + +
+ @if (alarmRuleFilterConfigForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+ } +} - + @if (buttonMode) { - + } @else { +
+
+
alarm-rule.alarm-rule-type-list
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + +
+ @if (alarmRuleFilterConfigForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+ }
@@ -70,9 +158,11 @@ {{ 'alarm-rule.any-type' | translate }} - - {{ entityTypeTranslations.get(type)?.type | translate }} - + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html index c7fd7da063..c883b20693 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html @@ -15,26 +15,28 @@ limitations under the License. --> -
- - - -
+@if (!standalone) { +
+ + + +
+}
@@ -55,11 +57,11 @@ + formControlName="debugSettings" + style="margin-bottom: 22px" + [entityType]="EntityType.CALCULATED_FIELD" + [additionalActionConfig]="additionalDebugActionConfig" + />
}
-
- alarm-rule.no-clear-alarm-rule -
+ @if (!configFormGroup.get('clearRule').value) { +
+ alarm-rule.no-clear-alarm-rule +
+ }
} +
-
{{ 'alarm-rule.type' | translate }}
- - {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} - + @for (alarmConditionType of alarmConditionTypes; track alarmConditionType) { + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + }
@if (isNoData) { @@ -157,13 +159,17 @@ - - {{ timeUnitTranslations.get(timeUnit) | translate }} - + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + } - - {{ 'alarm-rule.condition-duration-time-unit-required' | translate }} - + @if (conditionFormGroup.get('unit').hasError('required')) { + + {{ 'alarm-rule.condition-duration-time-unit-required' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html index f1d3ae9277..8858198d34 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html @@ -41,10 +41,12 @@ @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME) {
- - {{ dayOfWeekTranslationsArray[day] | translate }} - + @for (day of allDays; track day; let i = $index) { + + {{ dayOfWeekTranslationsArray[day] | translate }} + + } @@ -71,30 +73,32 @@ } @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM) {
-
-
- - {{ dayOfWeekTranslationsArray[day] | translate }} - -
- - alarm-rule.schedule-time-from - - - - - - alarm-rule.schedule-time-to - - - - -
-
+ @for (day of allDays; track day) { +
+
+ + {{ dayOfWeekTranslationsArray[day] | translate }} + +
+ + alarm-rule.schedule-time-from + + + + + + alarm-rule.schedule-time-to + + + + +
+
+
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html index 18ac406c97..bcf044e998 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -26,30 +26,34 @@ - - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + }
- + @if (!disabled) { + + }
} @if (!createAlarmRulesFormArray().controls.length) { - alarm-rule.add-create-alarm-rule-prompt + alarm-rule.add-create-alarm-rule-prompt }
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html index 8f6cd68c6a..2d971f98ab 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html @@ -53,14 +53,18 @@ {{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.name | translate }} - - - {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} - + @for (valueType of entityKeyValueTypesKeys; track valueType) { + + + {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} + + } - - {{ 'filter.value-type-required' | translate }} - + @if (filterFormGroup.get('valueType').hasError('required')) { + + {{ 'filter.value-type-required' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html index 09b468aeb9..b5206437c9 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html @@ -28,15 +28,15 @@ - @if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) { - - warning - - } + @if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) { + + warning + + } } @else { @@ -68,9 +68,11 @@ - - {{ timeUnitsTranslationMap.get(timeUnit) | translate }} - + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitsTranslationMap.get(timeUnit) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html index 273535d394..904e115225 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html @@ -22,9 +22,11 @@
- - {{stringOperationTranslationMap.get(stringOperation[operation]) | translate}} - + @for (operation of stringOperations; track operation) { + + {{stringOperationTranslationMap.get(stringOperation[operation]) | translate}} + + } @if (filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA) { @@ -38,9 +40,11 @@
- - {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} - + @for (operation of numericOperations; track operation) { + + {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} + + }
@@ -49,9 +53,11 @@
- - {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} - + @for (operation of booleanOperations; track operation) { + + {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} + + }
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html index c0f983e6d0..7adea3ffc1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html @@ -26,32 +26,42 @@ #userAutocomplete="matAutocomplete" [displayWith]="displayUserFn" (optionSelected)="selected($event)"> - - account_circle - {{ assigneeNotSetText | translate }} - - - account_circle - {{ assignedToCurrentUserText | translate }} - - - - -
- - -
-
- -
- - {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} + @if (displayAssigneeNotSet) { + + account_circle + {{ assigneeNotSetText | translate }} + + } + @if (displayAssignedToCurrentUser) { + + account_circle + {{ assignedToCurrentUserText | translate }} + + } + @for (user of filteredUsers | async; track user) { + + -
-
+
+ @if (user.firstName || user.lastName) { + + } + +
+ + } + @if (!(filteredUsers | async)?.length) { + +
+ + {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} + +
+
+ } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html index 8367d0eb4a..6148e955ad 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html @@ -19,13 +19,21 @@ - alarm.assignee + @if (!inline) { + alarm.assignee + } - - {{ getUserInitials() }} - - account_circle - arrow_drop_down + @if (assignee) { + + {{ getUserInitials() }} + + } + @if (!assignee) { + account_circle + } + @if (!disabled) { + arrow_drop_down + } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html index 783a763031..a566e9afb7 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html @@ -20,12 +20,13 @@ subscriptSizing="dynamic"> alarm.assignee - - {{ getUserInitials(alarm.assignee) }} - - + @if (userAssigned) { + + {{ getUserInitials(alarm.assignee) }} + + } @else { {{ alarm?.assigneeId ? 'no_accounts' : 'account_circle' }} - + } arrow_drop_down diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html index 651a1dd186..5f18a32d62 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html @@ -22,9 +22,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html index 25db54641d..31bf4dda43 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html @@ -49,104 +49,110 @@
- + @if (!isDirectionAscending()) { - + }
-
-
- - {{ displayDataElement.commentText }} - - - {{ displayDataElement.createdDateAgo }} - -
- -
-
- {{ getUserInitials(displayDataElement.displayName) }} -
- - no_accounts - -
-
- {{ displayDataElement.displayName }} - - {{ displayDataElement.createdDateAgo }} - - - Edited - -
- {{ displayDataElement.commentText }} -
-
- - + @for (displayDataElement of displayData; track displayDataElement; let i = $index) { +
+ @if (displayDataElement.isSystemComment) { +
+ + {{ displayDataElement.commentText }} + + + {{ displayDataElement.createdDateAgo }} +
-
- -
-
- {{ getUserInitials(displayDataElement.displayName) }} -
- - -
+ } @else { + @if (!displayDataElement.edit) { +
+ @if (displayDataElement.userExists) { +
+ {{ getUserInitials(displayDataElement.displayName) }} +
+ } @else { + no_accounts + } +
+
+ {{ displayDataElement.displayName }} + + {{ displayDataElement.createdDateAgo }} + + @if (displayDataElement.isEdited) { + + Edited + + } +
+ {{ displayDataElement.commentText }} +
+
- -
- - -
+
+ } @else { +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+ + +
+ + +
+
+
+ } + } +
+ }
- + @if (isDirectionAscending()) { - + }
-
- {{ getUserInitials(userDisplayName) }} -
+ @if (userDisplayName$ | async; as userDisplayName) { +
+ {{ getUserInitials(userDisplayName) }} +
+ } - + @if (getAlarmCommentValue()) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html index dbf649ec37..a47de5b3f7 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
@@ -43,14 +47,18 @@
- - alarm.start-time - - - - alarm.duration - - + @if (alarmFormGroup.get('startTime').value) { + + alarm.start-time + + + } + @if (alarmFormGroup.get('duration').value) { + + alarm.duration + + + }
@@ -71,12 +79,13 @@ - - + @if (displayDetails) { + + + }
@@ -88,25 +97,31 @@
-
- - -
+ @if (alarm$ | async; as alarm;) { +
+ @if (allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK || + alarm.status === alarmStatuses.CLEARED_UNACK)) { + + } + @if (allowClear && (alarm.status === alarmStatuses.ACTIVE_ACK || + alarm.status === alarmStatuses.ACTIVE_UNACK)) { + + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html index 966b3374b8..839dc49238 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html @@ -15,11 +15,65 @@ limitations under the License. --> - +@if (panelMode) { - +} @else { + @if (buttonMode) { + + } @else { +
+
+
alarm.alarm-status-list
+ + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + } + +
+
+
alarm.alarm-severity-list
+ + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + } + +
+
+
alarm.alarm-type-list
+ + +
+
+
alarm.assignee
+ + +
+ @if (propagatedFilter) { + + {{ 'alarm.search-propagated-alarms' | translate }} + + } +
+ } +} - + @if (buttonMode) { - + } @else { +
+
+
alarm.alarm-status-list
+ + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + } + +
+
+
alarm.alarm-severity-list
+ + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + } + +
+
+
alarm.alarm-type-list
+ + +
+
+
alarm.assignee
+ + +
+ @if (propagatedFilter) { + + {{ 'alarm.search-propagated-alarms' | translate }} + + } +
+ }
@@ -59,17 +157,21 @@
alarm.alarm-status-list
- - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - + @for (searchStatus of alarmSearchStatuses; track searchStatus) { + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + }
alarm.alarm-severity-list
- - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + }
@@ -85,11 +187,13 @@
alarm.assignee
+ formControlName="assigneeId">
- - {{ 'alarm.search-propagated-alarms' | translate }} - + @if (propagatedFilter) { + + {{ 'alarm.search-propagated-alarms' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html index 7dd028db41..adf52b3d39 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html @@ -22,23 +22,29 @@ formControlName="entityInfo" [required]="required" [matAutocomplete]="entityInfoAutocomplete"> - + @if (selectEntityInfoFormGroup.get('entityInfo').value && !disabled) { + + } - - - - - + @for (entityInfo of filteredEntityInfos | async; track entityInfo) { + + + + } + @if (!(filteredEntityInfos | async)?.length) { + + {{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }} - - + + + } diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html index d65b790222..4dafef12e4 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html @@ -16,13 +16,15 @@ -->
-
- - -
+ @for (alias of entityAliasesInfo | keyvalue; track alias) { +
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html index add27f87d3..fc1610dc8a 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html @@ -15,18 +15,20 @@ limitations under the License. --> -
- + - devices_other - - - {{displayValue}} - -
+ {{displayValue}} + + +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html index 4689bec48f..3394114d88 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html @@ -25,20 +25,26 @@ close - - + @if (isLoading$ | async) { + + + }
alias.name - - {{ 'alias.name-required' | translate }} - - - {{ 'alias.duplicate-alias' | translate }} - + @if (entityAliasFormGroup.get('alias').hasError('required')) { + + {{ 'alias.name-required' | translate }} + + } + @if (entityAliasFormGroup.get('alias').hasError('duplicateAliasName')) { + + {{ 'alias.duplicate-alias' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html index 3ed3b218bf..05aba8344a 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
@@ -40,45 +44,49 @@
-
- {{$index + 1}}. -
- - - - {{ 'entity.alias-required' | translate }} - - - - -
- - -
- - + @for (entityAliasControl of entityAliasesFormArray().controls; track entityAliasControl) { +
+ {{$index + 1}}. +
+ + + @if (entityAliasControl.get('alias').hasError('required')) { + + {{ 'entity.alias-required' | translate }} + + } + + + +
+ + +
+ + +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html index 738372a74e..05549f5260 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html @@ -36,9 +36,11 @@ api-key.description - - {{ 'asset.description-required' | translate }} - + @if (apiKeyForm.get('description').hasError('required')) { + + {{ 'asset.description-required' | translate }} + + } {{ 'api-key.enable' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html index 1a3f159af4..ff93fd0ea7 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html @@ -20,9 +20,11 @@ api-key.description - - {{ 'asset.description-required' | translate }} - + @if (descriptionFormControl.hasError('required')) { + + {{ 'asset.description-required' | translate }} + + } {{ input.value?.length || 0 }}/255
diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index fa791d479e..7028814b79 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
close - - + @if (isLoading$ | async) { + + + }
@@ -46,9 +48,11 @@ dashboard.new-dashboard-title - - {{ 'dashboard.title-required' | translate }} - + @if (addWidgetFormGroup.get('newDashboardTitle').hasError('required')) { + + {{ 'dashboard.title-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index 553349df8e..f53b87ad9d 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -21,34 +21,40 @@
{{telemetryTypeTranslationsMap.get(attributeScope) | translate}} - - attribute.attributes-scope - - - {{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }} - - - + @if (!disableAttributeScopeSelection && !attributeScopeSelectionReadonly) { + + attribute.attributes-scope + + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }} + + } + + + }
- - + @if (attributeScope !== attributeScopeTypes.CLIENT_SCOPE) { + + } + @if (!isClientSideTelemetryTypeMap.get(attributeScope)) { + + } - - - - - - -
-
- - - widgets-bundle.empty - widget.select-widgets-bundle + showFirstLastButtons> + @if (mode === 'widget' && widgetsList.length > 0) { + +
+ @for (widgets of widgetsList; track widgets) { +
+ + +
+ } +
+ + @if (widgetsCarouselIndex > 0) { + + } + + + @if (widgetsCarouselIndex < widgetsList.length - 1) { + + } + + +
+
+
+ } + @if (mode === 'widget' && widgetsLoaded && + widgetsList.length === 0 && + widgetBundleSet) { + widgets-bundle.empty + } + @if (mode === 'widget' && !widgetBundleSet) { + widget.select-widgets-bundle + }
diff --git a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html index 600d3f2a93..e2aca6e139 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html @@ -28,28 +28,34 @@ attribute.delete-timeseries.strategy - - {{ strategiesTranslationsMap.get(strategy) | translate }} - + @for (strategy of strategiesTranslationsMap.keys(); track strategy) { + + {{ strategiesTranslationsMap.get(strategy) | translate }} + + } -
- - attribute.delete-timeseries.start-time - - - - - - attribute.delete-timeseries.ends-on - - - - -
- - {{ "attribute.delete-timeseries.rewrite-latest-value" | translate }} - + @if (isPeriodStrategy()) { +
+ + attribute.delete-timeseries.start-time + + + + + + attribute.delete-timeseries.ends-on + + + + +
+ } + @if (isPeriodStrategy() || isDeleteLatestStrategy()) { + + {{ "attribute.delete-timeseries.rewrite-latest-value" | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html index 9eb8680594..86d183dcfd 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html @@ -15,26 +15,28 @@ limitations under the License. --> -
- - - -
+@if (!standalone) { +
+ + + +
+}
@@ -55,11 +57,11 @@ + formControlName="debugSettings" + style="margin-bottom: 22px" + [entityType]="EntityType.CALCULATED_FIELD" + [additionalActionConfig]="additionalDebugActionConfig" + />
{{ CalculatedFieldTypeTranslations.has(entityForm.get('type').value) - ? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate) - : entityForm.get('type').value }} + ? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate) + : entityForm.get('type').value }} @for (type of fieldTypes; track type) { @@ -83,67 +85,67 @@ {{ CalculatedFieldTypeTranslations.get(type).name | translate }}
- - {{ CalculatedFieldTypeTranslations.get(type).hint | translate }} - -
- } - - -
- @switch (entityForm.get('type').value) { - @case (CalculatedFieldType.GEOFENCING) { - - } - @case (CalculatedFieldType.PROPAGATION) { - - } - @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { - - } - @case (CalculatedFieldType.ENTITY_AGGREGATION) { - - } - @default { - + + {{ CalculatedFieldTypeTranslations.get(type).hint | translate }} + + + } + + +
+ @switch (entityForm.get('type').value) { + @case (CalculatedFieldType.GEOFENCING) { + + } + @case (CalculatedFieldType.PROPAGATION) { + + } + @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { + + } + @case (CalculatedFieldType.ENTITY_AGGREGATION) { + + } + @default { + + } } - } +
-
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html index ab7987c340..2288d5403d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html @@ -48,9 +48,11 @@ calculated-fields.aggregate-interval-type - - {{ AggIntervalTypeTranslations.get(type) | translate }} - + @for (type of AggIntervalTypes; track type) { + + {{ AggIntervalTypeTranslations.get(type) | translate }} + + }
{{ 'calculated-fields.entity-aggregation.produce-intermediate-result' | translate }}
@@ -140,5 +142,5 @@ simpleMode hiddenName disableType - [entityId]="entityId"> + [entityId]="entityId">
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html index bb191e8f26..2295c17ed1 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html @@ -93,90 +93,92 @@ } -
- - - {{ 'calculated-fields.entity-zone-relationship' | translate }} - -
-
-
-
calculated-fields.level
-
calculated-fields.direction-level
-
calculated-fields.relation-type
-
-
- @if (levelsFormArray()?.controls?.length) { -
- @for (keyControl of levelsFormArray().controls; track trackByKey; ) { -
-
- -
-
-
{{ $index + 1 }}
- - - @for (direction of GeofencingDirectionList; track direction) { - {{ GeofencingDirectionLevelTranslations.get(direction) | translate }} - } - - - - -
-
- -
-
- } -
- } @else { - {{ 'calculated-fields.no-level' | translate }} - } - @if (levelsFormArray().errors) { - - } -
-
- @if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) { -
- warning - {{ 'calculated-fields.max-allowed-levels-error' | translate }} + @if (entityType === ArgumentEntityType.RelationQuery) { +
+ + + {{ 'calculated-fields.entity-zone-relationship' | translate }} + +
+
+
+
calculated-fields.level
+
calculated-fields.direction-level
+
calculated-fields.relation-type
+
- } @else { - - } -
-
-
+ @if (levelsFormArray()?.controls?.length) { +
+ @for (keyControl of levelsFormArray().controls; track trackByKey; ) { +
+
+ +
+
+
{{ $index + 1 }}
+ + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionLevelTranslations.get(direction) | translate }} + } + + + + +
+
+ +
+
+ } +
+ } @else { + {{ 'calculated-fields.no-level' | translate }} + } + @if (levelsFormArray().errors) { + + } +
+
+ @if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) { +
+ warning + {{ 'calculated-fields.max-allowed-levels-error' | translate }} +
+ } @else { + + } +
+ +
+ } @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Owner) { 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 a1ccb56560..74075f56ee 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 @@ -15,11 +15,62 @@ limitations under the License. --> - +@if (panelMode) { - +} @else { + @if (buttonMode) { + + } @else { +
+
+
calculated-fields.calculated-field-types
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + +
+ @if (cfFilterForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+ } +} - + @if (buttonMode) { - + } @else { +
+
+
calculated-fields.calculated-field-types
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + +
+ @if (cfFilterForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+ }
@@ -72,9 +164,11 @@ {{ 'alarm-rule.any-type' | translate }} - - {{ entityTypeTranslations.get(type)?.type | translate }} - + @for (type of listEntityTypes; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index 0e1b2a7e07..ebd73b5388 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -21,11 +21,13 @@

{{'widget.add' | translate}}:

{{data.widgetInfo.widgetName}}
- - {{ 'widget.basic-mode' | translate }} - {{ 'widget.advanced-mode' | translate }} - + @if (widgetConfig.hasBasicMode) { + + {{ 'widget.basic-mode' | translate }} + {{ 'widget.advanced-mode' | translate }} + + }
@@ -49,22 +51,24 @@ isAdd formControlName="widgetConfig"> - -
- -
-
+ @if (previewMode) { + +
+ +
+
+ }
+ @if (!previewMode) { + + } - - + @if (isLoading$ | async) { + + + }
@@ -40,8 +42,11 @@
-
{{ 'dashboard.no-image' | translate }}
- + @if (!imageUrl) { +
{{ 'dashboard.no-image' | translate }}
+ } @else { + + }
Right % @@ -63,13 +68,15 @@
+ formControlName="dashboardImage">
-
+ @if (takingScreenshot$ | async) { +
-
+
+ }
- - + } + @if (showDashboardLogo()) { + + }
- - + @if (!isPublicUser() && forceFullscreen) { + + + } - + @if (!readonly && !isEdit) { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - > - - > - - - - - - - - - - - - - -
-
- - -
- - -
-

{{ translatedDashboardTitle }}

- - dashboard.title - - -
- - - - - - - - -
- {{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }} -
-
-
-
- - - -
- -
- - -
- - {{ 'widget.basic-mode' | translate }} - {{ 'widget.advanced-mode' | translate }} - -
-
- + edit{{'dashboard.edit-mode' | translate}} + } + @if (isEdit) { + + + + + + } + @if (currentDashboardId && isEdit && isTenantAdmin()) { + + + } + @if (isEdit) { + + + } + @if (isEdit) { + + + } + @if (isEdit) { + + + } + + @if (isEdit) { + + + + + } @else { + @if (displayDashboardTimewindow()) { + > + + > + + } + } + @if (isEdit) { + + + } + + + + + @if (!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()) { + + + } +
+
+ + +
+ + + @if (!widgetEditMode) { +
+

{{ translatedDashboardTitle }}

+ + dashboard.title + + +
+ } + + @if (layouts.right.show) { + + + + + } + + - - - + + @if (isEdit) { +
+ {{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }} +
+ } +
+
+ @if (!readonly && (hideToolbar || widgetEditMode)) { +
+ + + +
+ } + @if (!embedded) { + + } +
+ + @if (!isEditingWidgetClosed) { + +
+ @if (tbEditWidget.widgetConfig?.hasBasicMode) { + + {{ 'widget.basic-mode' | translate }} + {{ 'widget.advanced-mode' | translate }} + + } +
+
+ + +
+ } + @if (!isAddingWidgetClosed && !widgetEditMode) { + -
- -
-
- +
+ +
+ @if (isAddingWidget && dashboardWidgetSelectComponent) { +
+ - -
-
- - - {{ 'widgets-bundle.widgets-bundles' | translate }} - {{ 'widget.all-widgets' | translate }} - - - - {{ 'widget.all' | translate }} - {{ 'widget.actual' | translate }} - {{ 'widget.deprecated' | translate }} - -
- - - - - -
-
+ + + } + @if (isAddingWidget) { +
+ @if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) { + + } + @if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) { + + {{ 'widgets-bundle.widgets-bundles' | translate }} + {{ 'widget.all-widgets' | translate }} + + } + @if (dashboardWidgetSelectComponent?.widgetTypes.size > 1) { + + } + @if (dashboardWidgetSelectComponent?.hasDeprecated) { + + {{ 'widget.all' | translate }} + {{ 'widget.actual' | translate }} + {{ 'widget.deprecated' | translate }} + + } +
+ } + @if (isAddingWidget) { + + + } + + } + + + + diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index 70be4a9e55..7fb0f3f68d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -25,224 +25,258 @@ close - - + @if (isLoading$ | async) { + + + }
-
-
-
{{ 'dashboard.state-controller' | translate }}
- - - - {{ getStatesControllerTranslation(stateControllerId) }} - - - -
-
-
dashboard.title-settings
+ @if (settings) { +
- - {{ 'dashboard.title' | translate }} - - - -
-
-
-
dashboard.dashboard-logo-settings
-
- - {{ 'dashboard.display-dashboard-logo' | translate }} - -
- - -
-
-
dashboard.toolbar-settings
- - {{ 'dashboard.hide-toolbar' | translate }} - - - {{ 'dashboard.toolbar-always-open' | translate }} - - - {{ 'dashboard.display-dashboards-selection' | translate }} - - - {{ 'dashboard.display-entities-selection' | translate }} - - - {{ 'dashboard.display-filters' | translate }} - - - {{ 'dashboard.display-dashboard-timewindow' | translate }} - - - {{ 'dashboard.display-dashboard-export' | translate }} - - - {{ 'dashboard.display-update-dashboard-image' | translate }} - -
- - - - dashboard.advanced-settings - - - - - - -
-
-
-
dashboard.layout-settings-type
-
-
dashboard.view-format
+
{{ 'dashboard.state-controller' | translate }}
- - - {{ viewFormatTypeTranslationMap.get(type) | translate }} - + + @for (stateControllerId of stateControllerIds; track stateControllerId) { + + {{ getStatesControllerTranslation(stateControllerId) }} + + }
-
-
dashboard.columns-count
- +
dashboard.title-settings
+
+ + {{ 'dashboard.title' | translate }} + + + +
+
+
+
dashboard.dashboard-logo-settings
+
+ + {{ 'dashboard.display-dashboard-logo' | translate }} + +
+ + +
+
+
dashboard.toolbar-settings
+ + {{ 'dashboard.hide-toolbar' | translate }} + + + {{ 'dashboard.toolbar-always-open' | translate }} + + + {{ 'dashboard.display-dashboards-selection' | translate }} + + + {{ 'dashboard.display-entities-selection' | translate }} + + + {{ 'dashboard.display-filters' | translate }} + + + {{ 'dashboard.display-dashboard-timewindow' | translate }} + + + {{ 'dashboard.display-dashboard-export' | translate }} + + + {{ 'dashboard.display-update-dashboard-image' | translate }} + +
+ + + + dashboard.advanced-settings + + + + + + +
+ } + @if (gridSettings) { +
+
+
dashboard.layout-settings-type
+ @if (isDefaultLayout && !isDefaultBreakpoint) { +
+
dashboard.view-format
+ + + @for (type of viewFormatTypes; track type) { + + {{ viewFormatTypeTranslationMap.get(type) | translate }} + + } + + +
+ } +
+
dashboard.columns-count
+ @if (!isScada()) { + - - - - - - {{ columns }} - - - -
-
-
dashboard.min-layout-width
- + + } + @if (isScada()) { + + + @for (columns of scadaColumns; track columns) { + + {{ columns }} + + } + + + } +
+
+
dashboard.min-layout-width
+ - - dashboard.columns-suffix - -
-
-
dashboard.widgets-margins
- + dashboard.columns-suffix + +
+ @if (!isScada()) { +
+
dashboard.widgets-margins
+ - - -
-
- - {{ 'dashboard.apply-outer-margin' | translate }} - -
-
- - {{ 'dashboard.autofill-height' | translate }} - -
-
-
dashboard.row-height
- + +
+ } + @if (!isScada()) { +
+ + {{ 'dashboard.apply-outer-margin' | translate }} + +
+ } + @if (!isScada()) { +
+ + {{ 'dashboard.autofill-height' | translate }} + +
+ } + @if (isDefaultLayout && !isDefaultBreakpoint) { +
+
dashboard.row-height
+ - - px - -
-
-
dashboard.background-color
- - + + px + +
+ } +
+
dashboard.background-color
+ + +
+ + +
+
{{ 'dashboard.background-size-mode' | translate }}
+ + + Fit width + Fit height + Cover + Contain + Original size + + +
- - -
-
{{ 'dashboard.background-size-mode' | translate }}
- - - Fit width - Fit height - Cover - Contain - Original size - - -
-
-
-
dashboard.mobile-layout
-
- - {{ 'dashboard.display-first-in-mobile-view' | translate }} - -
-
- - {{ 'dashboard.autofill-height' | translate }} - -
-
-
dashboard.mobile-row-height
- +
dashboard.mobile-layout
+ @if (isRightLayout) { +
+ + {{ 'dashboard.display-first-in-mobile-view' | translate }} + +
+ } + @if (!isScada()) { +
+ + {{ 'dashboard.autofill-height' | translate }} + +
+ } + @if (!isScada()) { +
+
dashboard.mobile-row-height
+ - - px - -
+ + px +
+
+ } +
+ }
-
+ }
-
widget.deprecated
+ @if (item.deprecated) { +
widget.deprecated
+ }
@@ -64,51 +107,10 @@
- {{(selectWidgetMode === 'bundles' ? 'widgets-bundle.empty' : 'widget.no-widgets-text') | translate}} + {{(selectWidgetMode === 'bundles' ? 'widgets-bundle.empty' : 'widget.no-widgets-text') | translate}} - - - - - -
-
- {{item.title}} -
-
-
sys
-
i
-
-
-
-
- {{ item.title }} -
-
-
- -
- - {{ 'widgets-bundle.loading-widgets-bundles' | translate }} - - -
-
- - widgets-bundle.no-widgets-bundles-text - -
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html index 9b8e45c6dd..cc53c50fa9 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html @@ -28,56 +28,63 @@ [isDefaultBreakpoint]="isDefaultBreakpoint" formControlName="widgetConfig">
- - + @if (previewMode) { + + } + @if (!previewMode) { + + }
- + @if (!previewMode) { + + }
-
- - -
+ @if (previewMode) { +
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html index 230d930b05..14656f6b7c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html @@ -30,22 +30,26 @@ layout.breakpoint {{ getName(addBreakpointFormGroup.get('newBreakpointId').value) }} - - {{ getIcon(breakpoint) }} -
{{ getName(breakpoint) }}
-
{{ getSizeDescription(breakpoint) }}
-
+ @for (breakpoint of allowBreakpointIds; track breakpoint) { + + {{ getIcon(breakpoint) }} +
{{ getName(breakpoint) }}
+
{{ getSizeDescription(breakpoint) }}
+
+ }
layout.copy-from {{ getName(addBreakpointFormGroup.get('copyFrom').value) }} - - {{ getIcon(breakpoint) }} -
{{ getName(breakpoint) }}
-
{{ getSizeDescription(breakpoint) }}
-
+ @for (breakpoint of selectedBreakpointIds; track breakpoint) { + + {{ getIcon(breakpoint) }} +
{{ getName(breakpoint) }}
+
{{ getSizeDescription(breakpoint) }}
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html index ef892d7ac1..e07be60cb5 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html @@ -15,28 +15,36 @@ limitations under the License. --> -
- - -
+@if (layoutCtx.widgets.isLoading()) { +
+ + +
+}
-
- - {{'dashboard.no-widgets' | translate}} - - -
+ style="position: relative; width: 100%; height: 100%;" [tb-hotkeys]="hotKeys" + [cheatSheet]="dashboardCheatSheet" + [style.background-image]="backgroundImage$ | async" + [style]="dashboardStyle"> + @if (layoutCtx.widgets.isEmpty()) { +
+ @if (!isEdit) { + + {{'dashboard.no-widgets' | translate}} + + } + @if (isEdit && !widgetEditMode) { + + } +
+ } layout.manage @@ -31,201 +31,226 @@
dashboard.layout
- - {{ layoutTypeTranslations.get(type) | translate }} - + @for (type of layoutTypes; track type) { + + {{ layoutTypeTranslations.get(type) | translate }} + + }
-
-
-
-
-
layout.breakpoints
-
layout.size
-
-
-
- -
-
- {{ breakpoint.icon }} -
-
- {{ breakpoint.name }} -
-
- {{ breakpoint.descriptionSize }} + @if (!isDividerLayout) { +
+
+
+
+
layout.breakpoints
+
layout.size
+
+
+
+ @for (breakpoint of layoutBreakpoints; track breakpoint; let last = $last) { +
+
+ {{ breakpoint.icon }} +
+
+ {{ breakpoint.name }} +
+
+ {{ breakpoint.descriptionSize }} +
+
+ + +
-
+ @if (!last) { + + } + } +
+
+ +
+ } @else { +
+ + + {{ 'layout.percentage-width' | translate }} + + + {{ 'layout.fixed-width' | translate }} + + +
+
+
+ + + @if (showPreviewInputs('main')) { +
+ @if (layoutsFormGroup.get('type').value !== layoutWidthType.FIXED) { + + } + @if (layoutsFormGroup.get('type').value === layoutWidthType.FIXED && + layoutsFormGroup.get('fixedLayout').value === 'main') { + + } +
+ } +
+ @if (isDividerLayout) { +
- + @if (showPreviewInputs('right')) { +
+ @if (layoutsFormGroup.get('type').value !== layoutWidthType.FIXED) { + + } + @if (layoutsFormGroup.get('type').value === layoutWidthType.FIXED) { + + } +
+ }
-
- - -
-
- -
- - -
- - - {{ 'layout.percentage-width' | translate }} - - - {{ 'layout.fixed-width' | translate }} - - -
-
-
- - -
- - + }
-
- - -
- - + @if (isDividerLayout) { +
+ @if (layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE) { + + + } + @if (layoutsFormGroup.get('type').value !== layoutWidthType.PERCENTAGE) { + + + } +
+ + +
-
-
-
-
- - - - -
- - + }
-
-
- + } +
- - + @if (isLoading$ | async) { + + + }
dashboard.move-by
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html index d9e98fc48a..312cf304cd 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html @@ -21,9 +21,11 @@ [(ngModel)]="selectedBreakpoint" (ngModelChange)="selectLayoutChanged()"> {{ getName(selectedBreakpoint) }} - - {{ getIcon(breakpointId) }} -
{{ getName(breakpointId) }}
-
{{ getSizeDescription(breakpointId) }}
-
+ @for (breakpointId of breakpointIds; track breakpointId) { + + {{ getIcon(breakpointId) }} +
{{ getName(breakpointId) }}
+
{{ getSizeDescription(breakpointId) }}
+
+ } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html index c06d184162..ad0aeb097c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html @@ -25,26 +25,34 @@ close - - + @if (isLoading$ | async) { + + + }
dashboard.state-name - - {{ 'dashboard.state-name-required' | translate }} - + @if (stateFormGroup.get('name').hasError('required')) { + + {{ 'dashboard.state-name-required' | translate }} + + } dashboard.state-id - - {{ 'dashboard.state-id-required' | translate }} - - - {{ 'dashboard.state-id-exists' | translate }} - + @if (stateFormGroup.get('id').hasError('required')) { + + {{ 'dashboard.state-id-required' | translate }} + + } + @if (stateFormGroup.get('id').hasError('stateExists')) { + + {{ 'dashboard.state-id-exists' | translate }} + + } {{ 'dashboard.is-root-state' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html index fcf77163c9..a84fb44d26 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html @@ -16,8 +16,10 @@ --> - - {{getStateName(stateKv.key, stateKv.value)}} - + [(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()"> + @for (stateKv of states | keyvalue; track stateKv) { + + {{getStateName(stateKv.key, stateKv.value)}} + + } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html index 394bf14091..a932cd145e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html @@ -16,20 +16,28 @@ -->
-
- + @for (state of stateObject; track state; let i = $index; let isLast = $last) { + - {{getStateName(i)}} + {{getStateName(i)}} > -
- - - {{getStateName(i)}} - - + } +
+ } + @if (isMobile && stateObject.length > 1) { + + @for (state of stateObject; track state; let i = $index) { + + {{getStateName(i)}} + + } + + }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html index 51eebbf7ed..f26be70987 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html @@ -16,8 +16,10 @@ -->
- - {{ 'widget.' + type.type | translate }} - + @for (type of types; track type) { + + {{ 'widget.' + type.type | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index ef9d754ebb..5ba7c784b3 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -24,10 +24,10 @@
+ [class.center-vertical]="centerVertical" + [class.center-horizontal]="centerHorizontal" + (mousedown)="onDashboardMouseDown($event)" + (tbcontextmenu)="openDashboardContextMenu($event)">
- + @for (item of items; track item) { + + }
@@ -54,37 +60,45 @@
- + @for (item of items; track item) { + + }
- - - - + @for (widget of dashboardWidgets; track widget) { + + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html index c7524428e3..bf89db1b8d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html @@ -20,13 +20,15 @@

layout.select

- - + @if (isLoading$ | async) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html index a1ae9ba969..fdd4b84397 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html @@ -25,16 +25,20 @@ close - - + @if (isLoading$ | async) { + + + }
dashboard.state - - {{ stateItem.value.name }} - + @for (stateItem of states | keyvalue; track stateItem) { + + {{ stateItem.value.name }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.html b/ui-ngx/src/app/modules/home/components/details-panel.component.html index db9caf2cf3..eebb9a6db7 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.html @@ -28,41 +28,49 @@
-
+ @if (isShowSearch) { +
+ +
+ +
+
+ } + @if (isShowSearch && !showSearchPane) { -
- -
-
- + } - + @if (showCloseDetails) { + + }
-
- - -
+ @if (!isReadOnly) { +
+ + +
+ }
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 f2617c7fc8..3fe16989cf 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 @@ -15,11 +15,46 @@ limitations under the License. --> - +@if (panelMode) { - +} @else { + @if (buttonMode) { + + } @else { +
+
+
device-profile.device-profile
+ + +
+
+
device.device-state
+ + + {{ 'device.any' | translate }} + {{ 'device.active' | translate }} + {{ 'device.inactive' | translate }} + + +
+
+ } +} - + @if (buttonMode) { - + } @else { +
+
+
device-profile.device-profile
+ + +
+
+
device.device-state
+ + + {{ 'device.any' | translate }} + {{ 'device.active' | translate }} + {{ 'device.inactive' | translate }} + + +
+
+ }
@@ -59,13 +119,13 @@
device-profile.device-profile
+ class="flex-1" + formControlName="deviceProfileId" + [inlineField]="true" + [displayAllOnEmpty]="true" + (deviceProfileChanged)="deviceProfileChanged($event)" + [editProfileEnabled]="false" + [addNewProfile]="false">
diff --git a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html index 955f8f58be..b10c1b89db 100644 --- a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html @@ -26,9 +26,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html index 4d7ab8ee58..f23f7d00fc 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html @@ -36,14 +36,16 @@ {{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }}
- + @if ((isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 5b290980c6..c59c4cf5a9 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -16,37 +16,43 @@ --> - - - - + @if (entitiesTableConfig.detailsPanelEnabled) { + + + + + }
- {{ entitiesTableConfig.tableTitle }} + @if (entitiesTableConfig.tableTitle) { + {{ entitiesTableConfig.tableTitle }} + } - + @if (entitiesTableConfig.useTimePageLink) { + + }
- + @if (!entitiesTableConfig.addActionDescriptors.length) { - - - - - - + } @else { + @if (entitiesTableConfig.addActionDescriptors.length === 1) { + @if (entitiesTableConfig.addActionDescriptors[0].isEnabled()) { + + } + } @else { - - + } + }
- + @for (actionDescriptor of headerActionDescriptors; track actionDescriptor) { + + } - + @if (entitiesTableConfig.searchEnabled) { + + }
- + @if (!entitiesTableConfig.addActionDescriptors.length) { - - - - + } @else { + @if (entitiesTableConfig.addActionDescriptors.length === 1) { + @if (entitiesTableConfig.addActionDescriptors[0].isEnabled()) { - - - + } + } @else { - - + } + }
- + @for (actionDescriptor of entitiesTableConfig.addActionDescriptors; track actionDescriptor) { + + }
@@ -182,24 +192,28 @@
- -
- - {{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }} - - - -
-
+ @if (entitiesTableConfig.selectionEnabled) { + +
+ + {{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }} + + + @for (actionDescriptor of groupActionDescriptors; track actionDescriptor) { + + } +
+
+ }
+ matSort [matSortActive]="pageLink.sortOrder?.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> - - + @for (column of entityColumns; track column.key) { + + {{ column.ignoreTranslate ? column.title : (column.title | translate) }} - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ column.title | translate }} - - - - + + @switch (column.type) { + @case ('link') { + @if (column.entityURL(entity); as detailsPageURL) { + + } @else { + + } + } + @case ('entityChips') { + + } + @default { + + } + } + @if (column.actionCell) { + @switch (column.actionCell.type) { + @case (cellActionType.COPY_BUTTON) { + + + } + @default { + + } + } + } + + + } + @for (column of actionColumns; track trackByColumnKey($index, column)) { + + {{ column.title | translate }} + + + + + } - + @if (cellActionDescriptors.length) {
{{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
@@ -298,21 +320,22 @@ [style.min-width]="(cellActionDescriptors.length * 48) + 'px'"> {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }} -
+ }
- + @if (cellActionDescriptors.length) {
- + @for (actionDescriptor of cellActionDescriptors; track actionDescriptor) { + + }
- + @for (actionDescriptor of cellActionDescriptors; track actionDescriptor) { + + }
-
+ }
@@ -341,21 +366,25 @@ [class.mat-selected]="dataSource.selection.isSelected(entity)" [class.tb-current-entity]="dataSource.isCurrentEntity(entity)" [class.tb-pointer]="entitiesTableConfig.rowPointer" - *matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"> + *matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)">
{{ translations.noEntities | translate }} + class="no-data-found flex items-center justify-center">{{ translations.noEntities | translate }} {{ 'common.loading' | translate }} + class="no-data-found flex items-center justify-center">{{ 'common.loading' | translate }}
- - + @if (displayPagination) { + + } + @if (displayPagination) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 6999c76180..45fcf98eb0 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -49,7 +49,7 @@ import { CellActionDescriptorType, EntityActionTableColumn, EntityChipsEntityTableColumn, - EntityColumn, + EntityColumn, EntityColumnsType, EntityColumnType, EntityLinkTableColumn, EntityTableColumn, EntityTableConfig, @@ -89,8 +89,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa cellActionDescriptors: Array>>; actionColumns: Array>>; - entityColumns: Array>>; - + entityColumns: EntityColumnsType; displayedColumns: string[]; headerCellStyleCache: Array = []; @@ -594,8 +593,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa columnsUpdated(resetData: boolean = false) { this.entityColumns = this.entitiesTableConfig.columns.filter( (column) => column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn || - column instanceof EntityChipsEntityTableColumn) - .map(column => column as EntityTableColumn>); + column instanceof EntityChipsEntityTableColumn); this.actionColumns = this.entitiesTableConfig.columns.filter( (column) => column instanceof EntityActionTableColumn) .map(column => column as EntityActionTableColumn>); @@ -624,8 +622,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors]; } - headerCellStyle(column: EntityColumn>) { - const index = this.entitiesTableConfig.columns.indexOf(column); + headerCellStyle(column: EntityColumnType) { + const index = this.entitiesTableConfig.columns.indexOf(column as EntityColumn>); let res = this.headerCellStyleCache[index]; if (!res) { const widthStyle: any = {width: column.width}; @@ -650,7 +648,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.cellStyleCache[index] = undefined; } - cellContent(entity: BaseData, column: EntityColumn>, row: number) { + cellContent(entity: BaseData, column: EntityColumnType, row: number) { if (column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) { const col = this.entitiesTableConfig.columns.indexOf(column); const index = row * this.entitiesTableConfig.columns.length + col; @@ -665,7 +663,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } } - cellTooltip(entity: BaseData, column: EntityColumn>, row: number) { + cellTooltip(entity: BaseData, column: EntityColumnType, row: number) { if (column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) { const col = this.entitiesTableConfig.columns.indexOf(column); const index = row * this.entitiesTableConfig.columns.length + col; @@ -682,8 +680,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } } - cellStyle(entity: BaseData, column: EntityColumn>, row: number) { - const col = this.entitiesTableConfig.columns.indexOf(column); + cellStyle(entity: BaseData, column: EntityColumnType, row: number) { + const col = this.entitiesTableConfig.columns.indexOf(column as EntityColumn>); const index = row * this.entitiesTableConfig.columns.length + col; let res = this.cellStyleCache[index]; if (!res) { @@ -702,7 +700,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa return res; } - trackByColumnKey(index, column: EntityTableColumn>) { + trackByColumnKey(index, column: EntityTableColumn> | EntityActionTableColumn>) { return column.key; } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html index b0c7c71e00..856e56b9ea 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html @@ -15,7 +15,9 @@ limitations under the License. --> - - {{ subEntity.name | customTranslate }} - +@for (subEntity of subEntities; track subEntity) { + + {{ subEntity.name | customTranslate }} + +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html index 496566ed3a..070d80b4ab 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html @@ -30,25 +30,27 @@
-
- - -
+ @if (!isReadOnly) { +
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html index 333eafd7fa..319bb2fbf0 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html @@ -16,10 +16,11 @@ -->
-
alias.no-entity-filter-specified
- + @if (!filter || !filter.type) { +
alias.no-entity-filter-specified
+ } @else {
{{ filterDisplayValue }}
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index edbea5d1aa..ab517b2501 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -19,315 +19,330 @@ alias.filter-type - - {{aliasFilterTypeTranslations.get(type) | translate}} - + @for (type of aliasFilterTypes; track type) { + + {{aliasFilterTypeTranslations.get(type) | translate}} + + } - - {{ 'alias.filter-type-required' | translate }} - + @if (entityFilterFormGroup.get('type').hasError('required')) { + + {{ 'alias.filter-type-required' | translate }} + + } -
- - - - - - - - - - - - - - - entity.name-starts-with - help - - - {{ 'entity.entity-name-filter-required' | translate }} - - - - - - - - - - alias.state-entity-parameter-name - - -
-
{{ 'alias.default-state-entity' | translate }}
- + @switch (entityFilterFormGroup.get('type').value) { + @case (aliasFilterType.singleEntity) { + + formControlName="singleEntity"> -
-
- - - - - asset.name-starts-with - help - - - - - - - - device.name-starts-with - help - - - - - - - - entity-view.name-starts-with - help - - - - - - - - edge.name-starts-with - - - - -
-
-
alias.root-entity
- - {{ 'alias.root-state-entity' | translate }} - - @if (filterFormGroup.get('rootStateEntity').value) { -
- - alias.state-entity-parameter-name - - -
-
{{ 'alias.default-state-entity' | translate }}
- - -
-
- } @else { - - + } + @case (aliasFilterType.entityList) { + + + + + } + @case (aliasFilterType.entityName) { + + + + entity.name-starts-with + help + + @if (filterFormGroup.get('entityNameFilter').hasError('required')) { + + {{ 'entity.entity-name-filter-required' | translate }} + } + + } + @case (aliasFilterType.entityType) { + + + } + @case (aliasFilterType.stateEntity) { + + alias.state-entity-parameter-name + + +
+
{{ 'alias.default-state-entity' | translate }}
+ +
-
-
alias.query-options
-
- - relation.direction - - - {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} - - - - - alias.max-relation-level - - + } + @case (aliasFilterType.assetType) { + + + + asset.name-starts-with + help + + + } + @case (aliasFilterType.deviceType) { + + + + device.name-starts-with + help + + + } + @case (aliasFilterType.entityViewType) { + + + + entity-view.name-starts-with + help + + + } + @case (aliasFilterType.edgeType) { + + + + edge.name-starts-with + + + } + @case (aliasFilterType.relationsQuery) { +
+
+
alias.root-entity
+ + {{ 'alias.root-state-entity' | translate }} + + @if (filterFormGroup.get('rootStateEntity').value) { +
+ + alias.state-entity-parameter-name + + +
+
{{ 'alias.default-state-entity' | translate }}
+ + +
+
+ } @else { + + + }
- - {{ 'alias.last-level-relation' | translate }} - -
-
-
relation.relation-filters
- - -
-
-
- -
-
-
alias.root-entity
- - {{ 'alias.root-state-entity' | translate }} - - @if (filterFormGroup.get('rootStateEntity').value) { -
- - alias.state-entity-parameter-name - +
+
alias.query-options
+
+ + relation.direction + + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} + + } + -
-
{{ 'alias.default-state-entity' | translate }}
- - + + alias.max-relation-level + + +
+ @if (filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value) { + + {{ 'alias.last-level-relation' | translate }} + + } +
+
+
relation.relation-filters
+ + +
+
+ } + @case (entityFilterFormGroup.get('type').value === aliasFilterType.assetSearchQuery || + entityFilterFormGroup.get('type').value === aliasFilterType.deviceSearchQuery || + entityFilterFormGroup.get('type').value === aliasFilterType.edgeSearchQuery || + entityFilterFormGroup.get('type').value === aliasFilterType.entityViewSearchQuery ? + entityFilterFormGroup.get('type').value : '') { +
+
+
alias.root-entity
+ + {{ 'alias.root-state-entity' | translate }} + + @if (filterFormGroup.get('rootStateEntity').value) { +
+ + alias.state-entity-parameter-name + + +
+
{{ 'alias.default-state-entity' | translate }}
+ + +
+ } @else { + + + } +
+
+
alias.query-options
+
+ + relation.direction + + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} + + } + + + + alias.max-relation-level + +
- } @else { - - - } -
-
-
alias.query-options
-
- - relation.direction - - - {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} - - - - - alias.max-relation-level - - + @if (filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value) { + + {{ 'alias.last-level-relation' | translate }} + + }
- - {{ 'alias.last-level-relation' | translate }} - -
-
-
relation.relation-filter
- - - - - - - - - - - - - - - - - - - - -
-
-
- +
+
relation.relation-filter
+ + + @switch (entityFilterFormGroup.get('type').value) { + @case (aliasFilterType.assetSearchQuery) { + + + } + @case (aliasFilterType.deviceSearchQuery) { + + + } + @case (aliasFilterType.edgeSearchQuery) { + + + } + @case (aliasFilterType.entityViewSearchQuery) { + + + } + } +
+
+ } + }
diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html index df073b8702..f59cac1d7f 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html @@ -17,48 +17,52 @@ -->
- - - + @for (column of showColumns; track column) { + @switch (column.key) { + @case (isSelector(column.key)) { {{ column.title | translate}} {{ 'event.all-events' | translate}} - - {{ value }} - + @for (value of selectorValues(column.key); track value) { + + {{ value }} + + } - - + } + @case (isNumberFields(column.key)) { {{ column.title | translate}} - - {{ 'event.min-value' | translate }} - + @if (eventFilterFormGroup.get(column.key).hasError('min')) { + + {{ 'event.min-value' | translate }} + + } - - + } + @case ('isError') { {{ 'event.has-error' | translate }} - - + } + @case ('errorStr') { {{ column.title | translate}} - - + } + @default { {{ column.title | translate}} - - - + } + } + }
- + @if (!data.readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html index 230c8c8bb7..295b0c1d05 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html @@ -25,8 +25,10 @@ close - - + @if (isLoading$ | async) { + + + }
@@ -34,12 +36,16 @@ filter.name - - {{ 'filter.name-required' | translate }} - - - {{ 'filter.duplicate-filter' | translate }} - + @if (filterFormGroup.get('filter').hasError('required')) { + + {{ 'filter.name-required' | translate }} + + } + @if (filterFormGroup.get('filter').hasError('duplicateFilterName')) { + + {{ 'filter.duplicate-filter' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html index ff4f5de857..e4261076d5 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html @@ -28,49 +28,57 @@
- + @if (valueType === valueTypeEnum.STRING) { + + }
- + @if (displayUserParameters) { + + }  
-
-
- {{ complexOperationTranslations.get(operation) | translate }} -
-
-
- - - + @for (predicateControl of predicatesFormArray.controls; track predicateControl) { +
+
+ @if ($index > 0) { + {{ complexOperationTranslations.get(operation) | translate }} + } +
+
+
+ + + +
-
+ } filter.no-filters + class="no-data-found flex items-center justify-center" translate>filter.no-filters
- + @if (!onlyUserDynamicSource) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html index 89db536b53..037510fc9e 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html @@ -16,46 +16,50 @@ -->
-
- - - - - - - - - - - - - - - - +
+ @switch (type) { + @case (filterPredicateType.STRING) { + + + } + @case (filterPredicateType.NUMERIC) { + + + } + @case (filterPredicateType.BOOLEAN) { + + + } + @case (filterPredicateType.COMPLEX) { + + + } + }
- - + @if (type !== filterPredicateType.COMPLEX && displayUserParameters) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html index 52df01d33d..1190125501 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html @@ -50,11 +50,13 @@
-
-
filter.unit
- - -
+ @if (showUniInput) { +
+
filter.unit
+ + +
+ }
@@ -65,11 +67,12 @@ cdkFocusInitial> {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} - + @if (!data.readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html index 5251d87c4d..59c4c47eea 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html @@ -25,62 +25,68 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
filter.filter
filter.editable
+ style="padding-left: 5px;">filter.editable
-
- {{$index + 1}}. -
- {{filterControl.get('filter').value}} -
- - -
- - - + @for (filterControl of filtersFormArray().controls; track filterControl) { +
+ {{$index + 1}}. +
+ {{filterControl.get('filter').value}} +
+ + +
+ + + +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html index 6481c90d9c..3d4cdc1b77 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html @@ -16,18 +16,22 @@ -->
-
-
- {{filter.value.filter}} - + @for (filter of filtersInfo | keyvalue; track filter; let last = $last) { +
+
+ {{filter.value.filter}} + +
+ @if (!last) { + + }
- -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html index f70b00d863..f669c77b28 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html @@ -15,18 +15,20 @@ limitations under the License. --> -
- + - filter_list - - - {{ 'filter.filters' | translate }} - -
+ {{ 'filter.filters' | translate }} + + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html index d8e4264a8e..74d2e79290 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html @@ -33,13 +33,15 @@ 'max-w-45%': isConstantKeyType, 'max-w-70%': !isConstantKeyType }" - formGroupName="key"> + formGroupName="key"> filter.key-type.key-type - - {{entityKeyTypeTranslations.get(type) | translate}} - + @for (type of entityKeyTypes; track type) { + + {{entityKeyTypeTranslations.get(type) | translate}} + + } @@ -49,20 +51,26 @@ (focusin)="onFocus()" [matAutocomplete]="keyName" [matAutocompleteDisabled]="!showAutocomplete"> - + @if (keyFilterFormGroup.get('key.key').value && showAutocomplete) { + + } - - - + @for (keyName of filteredKeysName | async; track keyName) { + + + + } - - {{ 'filter.key-name-required' | translate }} - + @if (keyFilterFormGroup.get('key.key').hasError('required')) { + + {{ 'filter.key-name-required' | translate }} + + } {{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }} - - - {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} - + @for (valueType of entityKeyValueTypesKeys; track valueType) { + + + {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} + + } - - {{ 'filter.value-type-required' | translate }} - + @if (keyFilterFormGroup.get('valueType').hasError('required')) { + + {{ 'filter.value-type-required' | translate }} + + } -
-
- - - filter.value - - - - - - filter.value - - - - - - - - - {{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} - - + @if (isConstantKeyType) { +
+
+ @switch (keyFilterFormGroup.get('valueType').value) { + @case (entityKeyValueTypeEnum.STRING) { + + filter.value + + + } + @case (entityKeyValueTypeEnum.NUMERIC) { + + filter.value + + + } + @case (entityKeyValueTypeEnum.DATE_TIME) { + + } + @case (entityKeyValueTypeEnum.BOOLEAN) { + + {{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} + + } + } +
-
+ } - - + @if (keyFilterFormGroup.get('valueType').value) { + + + }
@@ -130,11 +148,12 @@ cdkFocusInitial> {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} - + @if (!data.readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html index ba5fbfc250..7f29298ba3 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html @@ -34,38 +34,42 @@
-
-
- filter.operation.and -
-
-
-
{{ keyFilterControl.value.key.key }}
-
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}
- - + @for (keyFilterControl of keyFiltersFormArray.controls; track keyFilterControl) { +
+
+ @if ($index > 0) { + filter.operation.and + } +
+
+
+
{{ keyFilterControl.value.key.key }}
+
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}
+ + +
+
-
-
+ } filter.no-key-filters + class="no-data-found flex items-center justify-center" translate>filter.no-key-filters
- - + @if (isLoading$ | async) { + + + }
-
-
- - - {{ userInputControl.get('label').value | customTranslate }} - - - - - - {{ userInputControl.get('label').value | customTranslate }} - -
- {{ userInputControl.get('unitSymbol').value }} -
-
-
- - - - - - - {{ userInputControl.get('label').value | customTranslate }} - - + @for (userInputControl of userInputsFormArray().controls; track userInputControl) { +
+
+ @switch (userInputControl.get('valueType').value) { + @case (valueTypeEnum.STRING) { + + {{ userInputControl.get('label').value | customTranslate }} + + + } + @case (valueTypeEnum.NUMERIC) { + + {{ userInputControl.get('label').value | customTranslate }} + +
+ {{ userInputControl.get('unitSymbol').value }} +
+
+ } + @case (valueTypeEnum.DATE_TIME) { + + + } + @case (valueTypeEnum.BOOLEAN) { + + {{ userInputControl.get('label').value | customTranslate }} + + } + } +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html index 0b04505520..ad9f187f76 100644 --- a/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html +++ b/ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html @@ -15,12 +15,14 @@ limitations under the License. --> -
- - -
+@if (show()) { +
+ + +
+} diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html index d58b525d5f..f05d9e372c 100644 --- a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html +++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html @@ -17,43 +17,52 @@ -->
notification.notification
- + @if ((notifications$ | async)?.length) { + + }
- -
-
-
- - - -
+@if (notifications$ | async) { + @if ((notifications$ | async).length) { +
+
+ @for (notification of (notifications$ | async); track notification.id.id; let last = $last) { +
+ + + @if (!last) { + + } +
+ } +
+
+ } @else { +
+ + + +
+ notification.no-notifications-yet + } + @if ((notifications$ | async).length) { + + } + @if ((notifications$ | async).length) { +
+
-
- -
- -
-
- -
- - - -
- notification.no-notifications-yet -
- + } +} @else {
notification.loading-notifications
-
+} diff --git a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts index f9e4969788..7bc4934629 100644 --- a/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts +++ b/ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts @@ -46,8 +46,8 @@ export class ShowNotificationPopoverComponent extends PageComponent implements O private notificationSubscriber = NotificationSubscriber.createNotificationsSubscription(this.notificationWsService, this.zone, 6); - notifications$: Observable = this.notificationSubscriber.notifications$.pipe( - filter(value => Array.isArray(value)), + notifications$: Observable = (this.notificationSubscriber.notifications$ as Observable).pipe( + filter((value): value is Notification[] => Array.isArray(value)), shareReplay(1), tap(() => setTimeout(() => this.cd.markForCheck())) ); @@ -91,8 +91,4 @@ export class ShowNotificationPopoverComponent extends PageComponent implements O this.onClose(); this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {}); } - - trackById(index: number, item: NotificationRequest): string { - return item.id.id; - } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html index 0fae730f16..39a5c625e3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html @@ -22,18 +22,26 @@ step="1" min="1" max="2147483647" formControlName="defaultValue" placeholder="{{ defaultValuePlaceholder | translate }}"> - - {{ defaultValueRequiredError | translate }} - - - {{ defaultValueRangeError | translate }} - - - {{ defaultValueRangeError | translate }} - - - {{ defaultValuePatternError | translate }} - + @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('required')) { + + {{ defaultValueRequiredError | translate }} + + } + @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('min')) { + + {{ defaultValueRangeError | translate }} + + } + @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('max')) { + + {{ defaultValueRangeError | translate }} + + } + @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('pattern')) { + + {{ defaultValuePatternError | translate }} + + }
@@ -44,9 +52,11 @@ {{'filter.no-dynamic-value' | translate}} - - {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} - + @for (sourceType of dynamicValueSourceTypes; track sourceType) { + + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} + + }
@@ -55,13 +65,15 @@
-
- - {{ 'filter.inherit-owner' | translate}} - -
+ @if (inheritMode) { +
+ + {{ 'filter.inherit-owner' | translate}} + +
+ }
- - + @if (isLoading$ | async) { + + + }
@@ -41,35 +43,47 @@ device-profile.condition-type - - {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} - + @for (alarmConditionType of alarmConditionTypes; track alarmConditionType) { + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + } - - {{ 'device-profile.condition-type-required' | translate }} - + @if (conditionFormGroup.get('spec.type').hasError('required')) { + + {{ 'device-profile.condition-type-required' | translate }} + + } -
- - -
- - - - {{ timeUnitTranslations.get(timeUnit) | translate }} - - - - {{ 'device-profile.condition-duration-time-unit-required' | translate }} - - + @if (conditionFormGroup.get('spec.type').value != AlarmConditionType.SIMPLE) { +
+ + + @if (conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION) { +
+ + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + } + + @if (conditionFormGroup.get('spec.unit').hasError('required')) { + + {{ 'device-profile.condition-duration-time-unit-required' | translate }} + + } + +
+ }
-
+ }
@@ -81,11 +95,12 @@ (click)="cancel()" cdkFocusInitial> {{ (readonly ? 'action.close' : 'action.cancel') | translate }} - + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html index 38d632b8ad..31cfb9aebb 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html @@ -23,8 +23,10 @@ required addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> - - + @if (specText) { + + + } - -
- - {{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }} - - - {{'device-profile.alarm-rule-mobile-dashboard-hint' | translate}} - -
+ @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { +
+ + {{ ('device-profile.alarm-rule-additional-info' | translate) + ': ' }} + + @if (alarmRuleFormGroup.get('alarmDetails').value) { + + } + +
+ } + @if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { +
+ + {{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }} + + + {{'device-profile.alarm-rule-mobile-dashboard-hint' | translate}} + +
+ } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html index 1e302d9063..141713832f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html @@ -25,8 +25,10 @@ close - - + @if (isLoading$ | async) { + + + }
@@ -39,11 +41,12 @@ (click)="cancel()" cdkFocusInitial> {{ (readonly ? 'action.close' : 'action.cancel') | translate }} - + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html index 1e0675900f..23226cd17a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html @@ -18,91 +18,106 @@
- - {{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} - + @for (alarmScheduleType of alarmScheduleTypes; track alarmScheduleType) { + + {{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} + + } - - {{ 'device-profile.schedule-type-required' | translate }} - + @if (alarmScheduleForm.get('type').hasError('required')) { + + {{ 'device-profile.schedule-type-required' | translate }} + + } -
- - -
- -
device-profile.schedule-days
-
-
- - {{ dayOfWeekTranslationsArray[day] | translate }} - -
-
- - {{ dayOfWeekTranslationsArray[day] | translate }} - -
-
+ @if (alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME) { +
+ + + @if (alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME) { +
+ +
device-profile.schedule-days
+
+
+ @for (day of firstRowDays; track day) { + + {{ dayOfWeekTranslationsArray[day] | translate }} + + } +
+
+ @for (day of secondRowDays; track day) { + + {{ dayOfWeekTranslationsArray[day] | translate }} + + } +
+
-
device-profile.schedule-time
-
-
- - device-profile.schedule-time-from - - - - - - device-profile.schedule-time-to - - - - -
-
-
-
-
-
-
-
- -
device-profile.schedule-days
-
-
- - {{ dayOfWeekTranslationsArray[day] | translate }} - -
- - device-profile.schedule-time-from - - - - - - device-profile.schedule-time-to - - - - +
device-profile.schedule-time
+
+
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
+
-
-
-
-
- +
+ } + @if (alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM) { +
+ +
device-profile.schedule-days
+ @for (day of allDays; track day) { +
+
+ + {{ dayOfWeekTranslationsArray[day] | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
+
+ } -
-
+
+ } +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts index 90caf04b70..e8224cabf3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts @@ -63,7 +63,7 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, alarmScheduleForm: UntypedFormGroup; - alarmScheduleTypes = Object.keys(AlarmScheduleType); + alarmScheduleTypes = Object.values(AlarmScheduleType); alarmScheduleType = AlarmScheduleType; alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; dayOfWeekTranslationsArray = dayOfWeekTranslations; diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html index d0c5d25273..e98fe00d2c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html @@ -16,49 +16,60 @@ -->
-
-
- - alarm.severity - - - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - - - - {{ 'device-profile.alarm-severity-required' | translate }} - - - - - + @for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let isLast = $last) { +
+
+ + alarm.severity + + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + } + + @if (createAlarmRuleControl.get('severity').hasError('required')) { + + {{ 'device-profile.alarm-severity-required' | translate }} + + } + + + + +
+ @if (!disabled) { + + }
- -
-
+ } + @if (!createAlarmRulesFormArray().controls.length && !disabled) { +
device-profile.add-create-alarm-rule-prompt -
-
- -
+ class="tb-prompt required">device-profile.add-create-alarm-rule-prompt +
+ } + @if (!disabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html index 3a9577df18..739f9e0716 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html @@ -24,13 +24,15 @@
- + @if (!disabled) { + + } @@ -39,15 +41,21 @@ {{'device-profile.alarm-type' | translate}} - - {{ 'device-profile.alarm-type-required' | translate }} - - - {{ 'device-profile.alarm-type-unique' | translate }} - - - {{ 'device-profile.alarm-type-max-length' | translate }} - + @if (alarmFormGroup.get('alarmType').hasError('required')) { + + {{ 'device-profile.alarm-type-required' | translate }} + + } + @if (alarmFormGroup.get('alarmType').hasError('unique')) { + + {{ 'device-profile.alarm-type-unique' | translate }} + + } + @if (alarmFormGroup.get('alarmType').hasError('maxlength')) { + + {{ 'device-profile.alarm-type-max-length' | translate }} + + } @@ -62,25 +70,28 @@ {{ 'device-profile.propagate-alarm' | translate }} -
- - device-profile.alarm-rule-relation-types-list - - - {{key}} - close - - - - - -
+ @if (alarmFormGroup.get('propagate').value === true) { +
+ + device-profile.alarm-rule-relation-types-list + + @for (key of alarmFormGroup.get('propagateRelationTypes').value; track key) { + + {{key}} + close + + } + + + + +
+ } {{ 'device-profile.propagate-alarm-to-owner' | translate }} @@ -103,29 +114,35 @@ - - -
- device-profile.no-clear-alarm-rule -
-
- + @if (!disabled) { + + }
+ @if (disabled && !alarmFormGroup.get('clearRule').value) { +
+ device-profile.no-clear-alarm-rule +
+ } + @if (!disabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html index 648217c6f5..71a8ad389e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html @@ -17,25 +17,30 @@ -->
-
- - -
+ @for (alarmControl of alarmsFormArray.controls; track trackByAlarm($index, alarmControl); let isLast = $last) { +
+ + +
+ }
-
+ @if (!alarmsFormArray.controls.length) { +
device-profile.no-alarm-rules -
-
- -
+ class="tb-prompt flex items-center justify-center">device-profile.no-alarm-rules +
+ } + @if (!disabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html index 7a94f51423..30caafd1aa 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html @@ -25,14 +25,16 @@ close - - + @if (isLoading$ | async) { + + + }
+ placeholder="{{ 'device-profile.alarm-rule-additional-info-placeholder' | translate }}">
@@ -45,10 +47,12 @@ (click)="cancel()" cdkFocusInitial> {{ 'action.cancel' | translate }} - + @if (!data.readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html index 9a349c3ef4..c699aea0a1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html @@ -26,56 +26,73 @@ (keypress)="assetProfileEnter($event)" [matAutocomplete]="assetProfileAutocomplete" [class.!hidden]="showDetailsPageLink && useAssetProfileLink && disabled && selectAssetProfileFormGroup.get('assetProfile').value"> - - {{ displayAssetProfileFn(selectAssetProfileFormGroup.get('assetProfile').value) }} - - - - + @if (showDetailsPageLink && useAssetProfileLink && selectAssetProfileFormGroup.get('assetProfile').value && disabled) { + + {{ displayAssetProfileFn(selectAssetProfileFormGroup.get('assetProfile').value) }} + + } + @if (selectAssetProfileFormGroup.get('assetProfile').value && !disabled) { + + } + @if (selectAssetProfileFormGroup.get('assetProfile').value?.id && !disabled && editProfileEnabled) { + + } + @if (!selectAssetProfileFormGroup.get('assetProfile').value && !disabled && addNewProfile) { + + } - - - - -
-
- asset-profile.no-asset-profiles-found -
- - + @for (assetProfile of filteredAssetProfiles | async; track assetProfile) { + + + + } + @if (!(filteredAssetProfiles | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText) || !addNewProfile) { +
+ asset-profile.no-asset-profiles-found +
+ } @else { + {{ translate.get('asset-profile.no-asset-profiles-matching', - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} - - - asset-profile.create-new-asset-profile - - -
-
+ {entity: truncate.transform(searchText, true, 6, '...')}) | async }} +
+ + asset-profile.create-new-asset-profile + + } +
+
+ }
- - {{ 'asset-profile.asset-profile-required' | translate }} - - {{ hint | translate }} + @if (selectAssetProfileFormGroup.get('assetProfile').hasError('required')) { + + {{ 'asset-profile.asset-profile-required' | translate }} + + } + @if (hint && !disabled) { + {{ hint | translate }} + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html index 724bc80e61..011b703fee 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
- {{ 'device-profile.device-profile' | translate }} + @if (!inlineField) { + {{ 'device-profile.device-profile' | translate }} + } - - {{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }} - - - - + @if (showDetailsPageLink && useDeviceProfileLink && selectDeviceProfileFormGroup.get('deviceProfile').value && disabled) { + + {{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }} + + } + @if (selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled) { + + } + @if (selectDeviceProfileFormGroup.get('deviceProfile').value?.id && !disabled && editProfileEnabled) { + + } + @if (!selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled && addNewProfile) { + + } - - - - -
-
- device-profile.no-device-profiles-found -
- - + @for (deviceProfile of filteredDeviceProfiles | async; track deviceProfile) { + + + + } + @if (!(filteredDeviceProfiles | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText) || !addNewProfile) { +
+ device-profile.no-device-profiles-found +
+ } @else { + {{ translate.get('device-profile.no-device-profiles-matching', - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} - - - device-profile.create-new-device-profile - - -
-
+ {entity: truncate.transform(searchText, true, 6, '...')}) | async }} +
+ + device-profile.create-new-device-profile + + } +
+
+ }
- - {{ 'device-profile.device-profile-required' | translate }} - - {{ hint | translate }} + @if (selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')) { + + {{ 'device-profile.device-profile-required' | translate }} + + } + @if (hint && !disabled) { + {{ hint | translate }} + } diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html index 246912b170..a180367e77 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
{{ labelText | translate }} - - {{ requiredText | translate }} - - - {{ patternText | translate }} - - - {{ (minText || patternText) | translate : {min: minTime/1000} }} - + @if (timeUnitSelectFormGroup.get('time').hasError('required')) { + + {{ requiredText | translate }} + + } + @if (timeUnitSelectFormGroup.get('time').hasError('pattern')) { + + {{ patternText | translate }} + + } + @if (timeUnitSelectFormGroup.get('time').hasError('min')) { + + {{ (minText || patternText) | translate : {min: minTime/1000} }} + + } device-profile.condition-duration-time-unit - - {{ timeUnitTranslations.get(timeUnit) | translate }} - + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html index 30cbfd8d36..61c5f4f9d4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html @@ -16,11 +16,13 @@ -->
-
- - - - +
+ @switch (type) { + @case (deviceProfileType.DEFAULT) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html index f8fdf1895d..84b7893cee 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html @@ -16,32 +16,34 @@ -->
-
- - - - - - - - - - - - - - - - - - - - +
+ @switch (transportType) { + @case (deviceTransportType.DEFAULT) { + + + } + @case (deviceTransportType.MQTT) { + + + } + @case (deviceTransportType.COAP) { + + + } + @case (deviceTransportType.LWM2M) { + + + } + @case (deviceTransportType.SNMP) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html index 7b4913446c..6a631c8df2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html @@ -27,8 +27,10 @@ close - - + @if (isLoading$ | async) { + + + }
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} - + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html index 7d82ef2581..e2ac4da12b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html @@ -21,51 +21,64 @@
-
-
- - - - {{ attributeNameTranslationMap.get(attributeName) | translate }} - - - - {{ 'device-profile.lwm2m.attribute-name-required' | translate }} - - - - - - {{ 'device-profile.lwm2m.attribute-value-required' | translate }} - - - {{ 'device-profile.lwm2m.attribute-value-pattern' | translate }} - - - + } +
+
+ } +
device-profile.lwm2m.no-attributes-set
+ @if (!disabled && isAddEnabled) { +
+
-
-
device-profile.lwm2m.no-attributes-set
-
- -
+ } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html index 468cb62a60..a966d6197b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component.html @@ -23,17 +23,23 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
{{ 'device-profile.lwm2m.server-type' | translate }} - - {{ serverConfigTypeNamesMap.get(serverType) | translate }} - + @for (serverType of serverTypes; track serverType) { + + {{ serverConfigTypeNamesMap.get(serverType) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html index 41ac62dae2..6f63601f0b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
{{ 'device-profile.lwm2m.instances-list' | translate }} - - {{instanceId}} - close - + @for (instanceId of instancesId; track instanceId) { + + {{instanceId}} + @if (!disabled) { + close + } + + } - - {{ 'device-profile.lwm2m.instances-list-required' | translate }} - - - {{ 'device-profile.lwm2m.instance-id-pattern' | translate }} - - - {{ 'device-profile.lwm2m.instance-id-max' | translate: {max: instanceIdValueMax} }} - + @if (instancesListFormGroup.get('instanceList').hasError('required')) { + + {{ 'device-profile.lwm2m.instances-list-required' | translate }} + + } + @if (instancesListFormGroup.get('instanceId').hasError('pattern')) { + + {{ 'device-profile.lwm2m.instance-id-pattern' | translate }} + + } + @if (instancesListFormGroup.get('instanceId').hasError('max')) { + + {{ 'device-profile.lwm2m.instance-id-max' | translate: {max: instanceIdValueMax} }} + + } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html index 1dea3c6a64..d56183f562 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html @@ -18,13 +18,16 @@ device-profile.lwm2m.object-list - - {{objectLwm2m.name}} #{{objectLwm2m.keyId}} - close - + @for (objectLwm2m of objectsList; track objectLwm2m) { + + {{objectLwm2m.name}} #{{objectLwm2m.keyId}} + @if (!disabled) { + close + } + + } - - - - -
- device-profile.lwm2m.no-objects-found -
- - + @for (objectLwm2m of filteredObjectsList | async; track objectLwm2m) { + + + + } + @if (!(filteredObjectsList | async)?.length) { + + @if (!textIsNotEmpty(searchText)) { +
+ device-profile.lwm2m.no-objects-found +
+ } @else { + {{ 'device-profile.lwm2m.no-objects-matching' | translate:{object: (searchText | truncate: true: 6: '...')} }} - -
-
+ + } + + } - - {{ 'device-profile.lwm2m.object-list-empty' | translate }} - + @if (lwm2mListFormGroup.get('objectsList').hasError('required')) { + + {{ 'device-profile.lwm2m.object-list-empty' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html index edc9e44bf8..acd00e3707 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html @@ -25,54 +25,71 @@
-
-
- - - - {{ dataTypesTranslationMap.get(dataType) | translate }} - - - - {{ 'device-profile.snmp.data-type-required' | translate }} - - - - - - {{ 'device-profile.snmp.data-key-required' | translate }} - - - - - - {{ 'device-profile.snmp.oid-required' | translate }} - - - {{ 'device-profile.snmp.oid-pattern' | translate }} - - - + } +
+
+ } + @if (!mappingsConfigFormArray.controls.length && !disabled) { +
+ device-profile.snmp.please-add-mapping-config +
+ } + @if (!disabled) { +
+
-
-
- device-profile.snmp.please-add-mapping-config -
-
- -
+ } diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html index 18019f3532..1c462bb9b7 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html @@ -16,40 +16,46 @@ -->
-
- - -
- - {{ getTitle(queuesControl.value.name) }} - - - -
-
- - - -
-
-
+ @for (queuesControl of queuesFormArray.controls; track trackByQueue($index, queuesControl); let isLast = $last) { +
+ + +
+ + {{ getTitle(queuesControl.value.name) }} + + + @if (!($index === 0) && !disabled) { + + } +
+
+ + + +
+
+ } + @if (!queuesFormArray.controls.length) { +
tenant-profile.no-queue -
-
- -
+ class="tb-prompt flex items-center justify-center">tenant-profile.no-queue +
+ } + @if (!disabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html index 791b8c1096..7b639f134c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html @@ -26,53 +26,68 @@ (keypress)="tenantProfileEnter($event)" [matAutocomplete]="tenantProfileAutocomplete" [class.!hidden]="showDetailsPageLink && disabled && selectTenantProfileFormGroup.get('tenantProfile').value"> - - {{ displayTenantProfileFn(selectTenantProfileFormGroup.get('tenantProfile').value) }} - - - - + @if (showDetailsPageLink && selectTenantProfileFormGroup.get('tenantProfile').value && disabled) { + + {{ displayTenantProfileFn(selectTenantProfileFormGroup.get('tenantProfile').value) }} + + } + @if (selectTenantProfileFormGroup.get('tenantProfile').value && !disabled) { + + } + @if (selectTenantProfileFormGroup.get('tenantProfile').value?.id && !disabled) { + + } + @if (!selectTenantProfileFormGroup.get('tenantProfile').value && !disabled) { + + } - - - - -
-
- tenant-profile.no-tenant-profiles-found -
- - + @for (tenantProfile of filteredTenantProfiles | async; track tenantProfile) { + + + + } + @if (!(filteredTenantProfiles | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ tenant-profile.no-tenant-profiles-found +
+ } @else { + {{ translate.get('tenant-profile.no-tenant-profiles-matching', - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + } + + tenant-profile.create-new-tenant-profile - - - tenant-profile.create-new-tenant-profile - -
-
+
+
+ }
- - {{ 'tenant-profile.tenant-profile-required' | translate }} - + @if (selectTenantProfileFormGroup.get('tenantProfile').hasError('required')) { + + {{ 'tenant-profile.tenant-profile-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html index 7f86c1d259..037e7eec22 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
-
- - - -
- + + +
+ +
-
+}
tenant-profile.name - - {{ 'tenant-profile.name-required' | translate }} - - - {{ 'tenant-profile.name-max-length' | translate }} - + @if (entityForm.get('name').hasError('required')) { + + {{ 'tenant-profile.name-required' | translate }} + + } + @if (entityForm.get('name').hasError('maxlength')) { + + {{ 'tenant-profile.name-max-length' | translate }} + + }
@@ -66,20 +72,22 @@
- - - - {{'tenant-profile.queues-with-count' | translate: + @if (entityForm.get('isolatedTbRuleEngine').value) { + + + + {{'tenant-profile.queues-with-count' | translate: {count: entityForm.get('profileData').get('queueConfiguration').value ? - entityForm.get('profileData').get('queueConfiguration').value.length : 0} }} - - - - - - + entityForm.get('profileData').get('queueConfiguration').value.length : 0} }} + + + + + + + } diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 19bd852f0a..7100fce348 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -26,12 +26,16 @@ - - {{ 'tenant-profile.maximum-devices-required' | translate}} - - - {{ 'tenant-profile.maximum-devices-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxDevices').hasError('required')) { + + {{ 'tenant-profile.maximum-devices-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxDevices').hasError('min')) { + + {{ 'tenant-profile.maximum-devices-range' | translate}} + + } @@ -39,12 +43,16 @@ - - {{ 'tenant-profile.maximum-dashboards-required' | translate}} - - - {{ 'tenant-profile.maximum-dashboards-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxDashboards').hasError('required')) { + + {{ 'tenant-profile.maximum-dashboards-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxDashboards').hasError('min')) { + + {{ 'tenant-profile.maximum-dashboards-range' | translate}} + + }
@@ -54,12 +62,16 @@ - - {{ 'tenant-profile.maximum-assets-required' | translate}} - - - {{ 'tenant-profile.maximum-assets-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxAssets').hasError('required')) { + + {{ 'tenant-profile.maximum-assets-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxAssets').hasError('min')) { + + {{ 'tenant-profile.maximum-assets-range' | translate}} + + } @@ -67,12 +79,16 @@ - - {{ 'tenant-profile.maximum-users-required' | translate}} - - - {{ 'tenant-profile.maximum-users-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxUsers').hasError('required')) { + + {{ 'tenant-profile.maximum-users-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxUsers').hasError('min')) { + + {{ 'tenant-profile.maximum-users-range' | translate}} + + }
@@ -89,12 +105,16 @@ - - {{ 'tenant-profile.maximum-customers-required' | translate}} - - - {{ 'tenant-profile.maximum-customers-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxCustomers').hasError('required')) { + + {{ 'tenant-profile.maximum-customers-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxCustomers').hasError('min')) { + + {{ 'tenant-profile.maximum-customers-range' | translate}} + + } @@ -102,12 +122,16 @@ - - {{ 'tenant-profile.maximum-rule-chains-required' | translate}} - - - {{ 'tenant-profile.maximum-rule-chains-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxRuleChains').hasError('required')) { + + {{ 'tenant-profile.maximum-rule-chains-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxRuleChains').hasError('min')) { + + {{ 'tenant-profile.maximum-rule-chains-range' | translate}} + + }
@@ -117,12 +141,16 @@ - - {{ 'tenant-profile.maximum-edges-required' | translate }} - - - {{ 'tenant-profile.maximum-edges-range' | translate }} - + @if (tenantProfileConfigurationForm.get('maxEdges').hasError('required')) { + + {{ 'tenant-profile.maximum-edges-required' | translate }} + + } + @if (tenantProfileConfigurationForm.get('maxEdges').hasError('min')) { + + {{ 'tenant-profile.maximum-edges-range' | translate }} + + }
@@ -141,12 +169,16 @@ - - {{ 'tenant-profile.max-r-e-executions-required' | translate}} - - - {{ 'tenant-profile.max-r-e-executions-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxREExecutions').hasError('required')) { + + {{ 'tenant-profile.max-r-e-executions-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxREExecutions').hasError('min')) { + + {{ 'tenant-profile.max-r-e-executions-range' | translate}} + + } @@ -154,12 +186,16 @@ - - {{ 'tenant-profile.max-transport-messages-required' | translate}} - - - {{ 'tenant-profile.max-transport-messages-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxTransportMessages').hasError('required')) { + + {{ 'tenant-profile.max-transport-messages-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxTransportMessages').hasError('min')) { + + {{ 'tenant-profile.max-transport-messages-range' | translate}} + + } @@ -176,12 +212,16 @@ - - {{ 'tenant-profile.max-j-s-executions-required' | translate}} - - - {{ 'tenant-profile.max-j-s-executions-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxJSExecutions').hasError('required')) { + + {{ 'tenant-profile.max-j-s-executions-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxJSExecutions').hasError('min')) { + + {{ 'tenant-profile.max-j-s-executions-range' | translate}} + + } @@ -189,12 +229,16 @@ - - {{ 'tenant-profile.max-tbel-executions-required' | translate}} - - - {{ 'tenant-profile.max-tbel-executions-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxTbelExecutions').hasError('required')) { + + {{ 'tenant-profile.max-tbel-executions-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxTbelExecutions').hasError('min')) { + + {{ 'tenant-profile.max-tbel-executions-range' | translate}} + + } @@ -204,12 +248,16 @@ - - {{ 'tenant-profile.max-rule-node-executions-per-message-required' | translate}} - - - {{ 'tenant-profile.max-rule-node-executions-per-message-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxRuleNodeExecutionsPerMessage').hasError('required')) { + + {{ 'tenant-profile.max-rule-node-executions-per-message-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxRuleNodeExecutionsPerMessage').hasError('min')) { + + {{ 'tenant-profile.max-rule-node-executions-per-message-range' | translate}} + + } @@ -217,12 +265,16 @@ - - {{ 'tenant-profile.max-transport-data-points-required' | translate}} - - - {{ 'tenant-profile.max-transport-data-points-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxTransportDataPoints').hasError('required')) { + + {{ 'tenant-profile.max-transport-data-points-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxTransportDataPoints').hasError('min')) { + + {{ 'tenant-profile.max-transport-data-points-range' | translate}} + + } @@ -239,12 +291,16 @@ - - {{ 'tenant-profile.max-calculated-fields-required' | translate}} - - - {{ 'tenant-profile.max-calculated-fields-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxCalculatedFieldsPerEntity').hasError('required')) { + + {{ 'tenant-profile.max-calculated-fields-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxCalculatedFieldsPerEntity').hasError('min')) { + + {{ 'tenant-profile.max-calculated-fields-range' | translate}} + + } @@ -252,12 +308,16 @@ - - {{ 'tenant-profile.max-data-points-per-rolling-arg-required' | translate}} - - - {{ 'tenant-profile.max-data-points-per-rolling-arg-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxDataPointsPerRollingArg').hasError('required')) { + + {{ 'tenant-profile.max-data-points-per-rolling-arg-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxDataPointsPerRollingArg').hasError('min')) { + + {{ 'tenant-profile.max-data-points-per-rolling-arg-range' | translate}} + + } @@ -267,12 +327,16 @@ - - {{ 'tenant-profile.max-arguments-per-cf-required' | translate}} - - - {{ 'tenant-profile.max-arguments-per-cf-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxArgumentsPerCF').hasError('required')) { + + {{ 'tenant-profile.max-arguments-per-cf-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxArgumentsPerCF').hasError('min')) { + + {{ 'tenant-profile.max-arguments-per-cf-range' | translate}} + + }
@@ -290,12 +354,16 @@ - - {{ 'tenant-profile.max-state-size-required' | translate}} - - - {{ 'tenant-profile.max-state-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxStateSizeInKBytes').hasError('required')) { + + {{ 'tenant-profile.max-state-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxStateSizeInKBytes').hasError('min')) { + + {{ 'tenant-profile.max-state-size-range' | translate}} + + } @@ -303,12 +371,16 @@ - - {{ 'tenant-profile.max-value-argument-size-required' | translate}} - - - {{ 'tenant-profile.max-value-argument-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxSingleValueArgumentSizeInKBytes').hasError('required')) { + + {{ 'tenant-profile.max-value-argument-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxSingleValueArgumentSizeInKBytes').hasError('min')) { + + {{ 'tenant-profile.max-value-argument-size-range' | translate}} + + } @@ -318,12 +390,16 @@ - - {{ 'tenant-profile.max-related-level-per-argument-required' | translate}} - - - {{ 'tenant-profile.max-related-level-per-argument-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxRelationLevelPerCfArgument').hasError('required')) { + + {{ 'tenant-profile.max-related-level-per-argument-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxRelationLevelPerCfArgument').hasError('min')) { + + {{ 'tenant-profile.max-related-level-per-argument-range' | translate}} + + } @@ -331,12 +407,16 @@ - - {{ 'tenant-profile.min-allowed-scheduled-update-interval-required' | translate}} - - - {{ 'tenant-profile.min-allowed-scheduled-update-interval-range' | translate}} - + @if (tenantProfileConfigurationForm.get('minAllowedScheduledUpdateIntervalInSecForCF').hasError('required')) { + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('minAllowedScheduledUpdateIntervalInSecForCF').hasError('min')) { + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-range' | translate}} + + } @@ -346,12 +426,16 @@ - - {{ 'tenant-profile.min-allowed-aggregation-interval-required' | translate}} - - - {{ 'tenant-profile.min-allowed-aggregation-interval-range' | translate}} - + @if (tenantProfileConfigurationForm.get('minAllowedAggregationIntervalInSecForCF').hasError('required')) { + + {{ 'tenant-profile.min-allowed-aggregation-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('minAllowedAggregationIntervalInSecForCF').hasError('min')) { + + {{ 'tenant-profile.min-allowed-aggregation-interval-range' | translate}} + + } @@ -359,12 +443,16 @@ - - {{ 'tenant-profile.min-allowed-deduplication-interval-required' | translate}} - - - {{ 'tenant-profile.min-allowed-deduplication-interval-range' | translate}} - + @if (tenantProfileConfigurationForm.get('minAllowedDeduplicationIntervalInSecForCF').hasError('required')) { + + {{ 'tenant-profile.min-allowed-deduplication-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('minAllowedDeduplicationIntervalInSecForCF').hasError('min')) { + + {{ 'tenant-profile.min-allowed-deduplication-interval-range' | translate}} + + } @@ -374,12 +462,16 @@ - - {{ 'tenant-profile.intermediate-aggregation-interval-required' | translate}} - - - {{ 'tenant-profile.intermediate-aggregation-interval-range' | translate}} - + @if (tenantProfileConfigurationForm.get('intermediateAggregationIntervalInSecForCF').hasError('required')) { + + {{ 'tenant-profile.intermediate-aggregation-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('intermediateAggregationIntervalInSecForCF').hasError('min')) { + + {{ 'tenant-profile.intermediate-aggregation-interval-range' | translate}} + + } @@ -387,12 +479,16 @@ - - {{ 'tenant-profile.reevaluation-check-interval-required' | translate}} - - - {{ 'tenant-profile.reevaluation-check-interval-range' | translate}} - + @if (tenantProfileConfigurationForm.get('cfReevaluationCheckInterval').hasError('required')) { + + {{ 'tenant-profile.reevaluation-check-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('cfReevaluationCheckInterval').hasError('min')) { + + {{ 'tenant-profile.reevaluation-check-interval-range' | translate}} + + } @@ -402,12 +498,16 @@ - - {{ 'tenant-profile.relation-search-entity-limit-required' | translate}} - - - {{ 'tenant-profile.relation-search-entity-limit-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxRelatedEntitiesToReturnPerCfArgument').hasError('required')) { + + {{ 'tenant-profile.relation-search-entity-limit-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxRelatedEntitiesToReturnPerCfArgument').hasError('min')) { + + {{ 'tenant-profile.relation-search-entity-limit-range' | translate}} + + } tenant-profile.relation-search-entity-limit-hint
@@ -426,12 +526,16 @@ - - {{ 'tenant-profile.max-d-p-storage-days-required' | translate}} - - - {{ 'tenant-profile.max-d-p-storage-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxDPStorageDays').hasError('required')) { + + {{ 'tenant-profile.max-d-p-storage-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxDPStorageDays').hasError('min')) { + + {{ 'tenant-profile.max-d-p-storage-days-range' | translate}} + + } @@ -439,12 +543,16 @@ - - {{ 'tenant-profile.alarms-ttl-days-required' | translate}} - - - {{ 'tenant-profile.alarms-ttl-days-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('alarmsTtlDays').hasError('required')) { + + {{ 'tenant-profile.alarms-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('alarmsTtlDays').hasError('min')) { + + {{ 'tenant-profile.alarms-ttl-days-days-range' | translate}} + + } @@ -454,12 +562,16 @@ - - {{ 'tenant-profile.default-storage-ttl-days-required' | translate}} - - - {{ 'tenant-profile.default-storage-ttl-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('defaultStorageTtlDays').hasError('required')) { + + {{ 'tenant-profile.default-storage-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('defaultStorageTtlDays').hasError('min')) { + + {{ 'tenant-profile.default-storage-ttl-days-range' | translate}} + + } @@ -467,12 +579,16 @@ - - {{ 'tenant-profile.rpc-ttl-days-required' | translate}} - - - {{ 'tenant-profile.rpc-ttl-days-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('rpcTtlDays').hasError('required')) { + + {{ 'tenant-profile.rpc-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('rpcTtlDays').hasError('min')) { + + {{ 'tenant-profile.rpc-ttl-days-days-range' | translate}} + + } @@ -482,12 +598,16 @@ - - {{ 'tenant-profile.queue-stats-ttl-days-required' | translate}} - - - {{ 'tenant-profile.queue-stats-ttl-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('queueStatsTtlDays').hasError('required')) { + + {{ 'tenant-profile.queue-stats-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('queueStatsTtlDays').hasError('min')) { + + {{ 'tenant-profile.queue-stats-ttl-days-range' | translate}} + + } @@ -495,12 +615,16 @@ - - {{ 'tenant-profile.rule-engine-exceptions-ttl-days-required' | translate}} - - - {{ 'tenant-profile.rule-engine-exceptions-ttl-days-range' | translate}} - + @if (tenantProfileConfigurationForm.get('ruleEngineExceptionsTtlDays').hasError('required')) { + + {{ 'tenant-profile.rule-engine-exceptions-ttl-days-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('ruleEngineExceptionsTtlDays').hasError('min')) { + + {{ 'tenant-profile.rule-engine-exceptions-ttl-days-range' | translate}} + + } @@ -513,31 +637,41 @@ {{ 'tenant-profile.sms-enabled' | translate }} - - tenant-profile.max-sms - - - {{ 'tenant-profile.max-sms-required' | translate}} - - - {{ 'tenant-profile.max-sms-range' | translate}} - - + @if (tenantProfileConfigurationForm.get('smsEnabled').value) { + + tenant-profile.max-sms + + @if (tenantProfileConfigurationForm.get('maxSms').hasError('required')) { + + {{ 'tenant-profile.max-sms-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxSms').hasError('min')) { + + {{ 'tenant-profile.max-sms-range' | translate}} + + } + + }
tenant-profile.max-emails - - {{ 'tenant-profile.max-emails-required' | translate}} - - - {{ 'tenant-profile.max-emails-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxEmails').hasError('required')) { + + {{ 'tenant-profile.max-emails-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxEmails').hasError('min')) { + + {{ 'tenant-profile.max-emails-range' | translate}} + + } @@ -545,12 +679,16 @@ - - {{ 'tenant-profile.max-created-alarms-required' | translate}} - - - {{ 'tenant-profile.max-created-alarms-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxCreatedAlarms').hasError('required')) { + + {{ 'tenant-profile.max-created-alarms-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxCreatedAlarms').hasError('min')) { + + {{ 'tenant-profile.max-created-alarms-range' | translate}} + + }
@@ -560,12 +698,16 @@ - - {{ 'tenant-profile.alarms-reevaluation-interval-required' | translate}} - - - {{ 'tenant-profile.alarms-reevaluation-interval-range' | translate}} - + @if (tenantProfileConfigurationForm.get('alarmsReevaluationInterval').hasError('required')) { + + {{ 'tenant-profile.alarms-reevaluation-interval-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('alarmsReevaluationInterval').hasError('min')) { + + {{ 'tenant-profile.alarms-reevaluation-interval-range' | translate}} + + }
@@ -582,9 +724,11 @@ - - {{ 'tenant-profile.maximum-debug-duration-min-range' | translate }} - + @if (tenantProfileConfigurationForm.get('maxDebugModeDurationMinutes').hasError('min')) { + + {{ 'tenant-profile.maximum-debug-duration-min-range' | translate }} + + }
@@ -601,12 +745,16 @@ - - {{ 'tenant-profile.maximum-resources-sum-data-size-required' | translate}} - - - {{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxResourcesInBytes').hasError('required')) { + + {{ 'tenant-profile.maximum-resources-sum-data-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxResourcesInBytes').hasError('min')) { + + {{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}} + + } @@ -614,12 +762,16 @@ - - {{ 'tenant-profile.maximum-ota-package-sum-data-size-required' | translate}} - - - {{ 'tenant-profile.maximum-ota-package-sum-data-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxOtaPackagesInBytes').hasError('required')) { + + {{ 'tenant-profile.maximum-ota-package-sum-data-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxOtaPackagesInBytes').hasError('min')) { + + {{ 'tenant-profile.maximum-ota-package-sum-data-size-range' | translate}} + + } @@ -629,12 +781,16 @@ - - {{ 'tenant-profile.maximum-resource-size-required' | translate}} - - - {{ 'tenant-profile.maximum-resource-size-range' | translate}} - + @if (tenantProfileConfigurationForm.get('maxResourceSize').hasError('required')) { + + {{ 'tenant-profile.maximum-resource-size-required' | translate}} + + } + @if (tenantProfileConfigurationForm.get('maxResourceSize').hasError('min')) { + + {{ 'tenant-profile.maximum-resource-size-range' | translate}} + + }
@@ -649,32 +805,40 @@ tenant-profile.ws-limit-max-sessions-per-tenant - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSessionsPerTenant').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + } tenant-profile.ws-limit-max-subscriptions-per-tenant - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerTenant').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + }
tenant-profile.ws-limit-max-sessions-per-customer - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSessionsPerCustomer').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + } tenant-profile.ws-limit-max-subscriptions-per-customer - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerCustomer').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + }
@@ -688,41 +852,51 @@ tenant-profile.ws-limit-max-sessions-per-public-user - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSessionsPerPublicUser').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + } tenant-profile.ws-limit-max-subscriptions-per-public-user - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerPublicUser').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + }
tenant-profile.ws-limit-max-sessions-per-regular-user - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSessionsPerRegularUser').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + } tenant-profile.ws-limit-max-subscriptions-per-regular-user - - {{ 'tenant-profile.too-small-value-zero' | translate}} - + @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerRegularUser').hasError('min')) { + + {{ 'tenant-profile.too-small-value-zero' | translate}} + + }
tenant-profile.ws-limit-queue-per-session - - {{ 'tenant-profile.too-small-value-one' | translate}} - + @if (tenantProfileConfigurationForm.get('wsMsgQueueLimitPerSession').hasError('min')) { + + {{ 'tenant-profile.too-small-value-one' | translate}} + + } {{ 'tenant-profile.queue-size-is-limited-by-system-configuration' | translate}} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html index 5d7fc9876f..370849422e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html @@ -35,9 +35,11 @@ cdkFocusInitial> {{ 'action.cancel' | translate }} - + @if (!data.readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html index 617a8beaee..d24cc15a4b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html @@ -16,51 +16,56 @@ -->
-
- @if($index > 0) { -
tenant-profile.rate-limits.and-also-less-than
- } -
- - tenant-profile.rate-limits.number-of-messages - - - @if (rateLimit.get('value').hasError('required')) { - tenant-profile.rate-limits.number-of-messages-required - } @else if(rateLimit.get('value').hasError('min')) { - tenant-profile.rate-limits.number-of-messages-min + @for (rateLimit of rateLimitsFormArray.controls; track rateLimit) { +
+ @if($index > 0) { +
tenant-profile.rate-limits.and-also-less-than
+ } +
+ + tenant-profile.rate-limits.number-of-messages + + + @if (rateLimit.get('value').hasError('required')) { + tenant-profile.rate-limits.number-of-messages-required + } @else if(rateLimit.get('value').hasError('min')) { + tenant-profile.rate-limits.number-of-messages-min + } + + + tenant-profile.rate-limits.per-seconds + + + @if (rateLimit.get('time').hasError('required')) { + tenant-profile.rate-limits.per-seconds-required + } @else if (rateLimit.get('time').hasError('min')) { + tenant-profile.rate-limits.per-seconds-min + } @else if (rateLimit.get('time').hasError('duplicateTime')) { + tenant-profile.rate-limits.per-seconds-duplicate + } + + @if (!rateLimitsFormArray.disabled) { + } - - - tenant-profile.rate-limits.per-seconds - - - @if (rateLimit.get('time').hasError('required')) { - tenant-profile.rate-limits.per-seconds-required - } @else if (rateLimit.get('time').hasError('min')) { - tenant-profile.rate-limits.per-seconds-min - } @else if (rateLimit.get('time').hasError('duplicateTime')) { - tenant-profile.rate-limits.per-seconds-duplicate - } - - +
-
- + } + @if (!rateLimitsFormArray.disabled) { + + }
tenant-profile.rate-limits.preview diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html index 39d97ae424..f0dd188aa6 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html @@ -24,10 +24,12 @@
- + @if (!disabled) { + + } diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.html index a99101afa3..a72c51f430 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/tenant-profile-configuration.component.html @@ -16,12 +16,14 @@ -->
-
- - - - +
+ @switch (type) { + @case (tenantProfileType.DEFAULT) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html index 1dd2a79e27..de7b1de846 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html @@ -19,15 +19,21 @@ admin.queue-name - - {{ 'queue.name-required' | translate }} - - - {{ 'queue.name-unique' | translate }} - - - {{ 'queue.name-pattern' | translate }} - + @if (queueFormGroup.get('name').hasError('required')) { + + {{ 'queue.name-required' | translate }} + + } + @if (queueFormGroup.get('name').hasError('unique')) { + + {{ 'queue.name-unique' | translate }} + + } + @if (queueFormGroup.get('name').hasError('pattern')) { + + {{ 'queue.name-pattern' | translate }} + + } @@ -41,30 +47,38 @@
- - {{ queueSubmitStrategyTypesMap.get(queueSubmitStrategyTypes[strategy]).label | translate }} - - - + @for (strategy of submitStrategies; track strategy) { + + {{ queueSubmitStrategyTypesMap.get(queueSubmitStrategyTypes[strategy]).label | translate }} + + + + }
-
- - - queue.batch-size - - - {{ 'queue.batch-size-required' | translate }} - - - {{ 'queue.batch-size-min-value' | translate }} - - -
+ @if (hideBatchSize) { +
+ + + queue.batch-size + + @if (queueFormGroup.get('submitStrategy.batchSize').hasError('required')) { + + {{ 'queue.batch-size-required' | translate }} + + } + @if (queueFormGroup.get('submitStrategy.batchSize').hasError('min') && + !queueFormGroup.get('submitStrategy.batchSize').hasError('required')) { + + {{ 'queue.batch-size-min-value' | translate }} + + } + +
+ }
@@ -78,8 +92,9 @@
- - + + @for (strategy of processingStrategies; track strategy) { + {{ queueProcessingStrategyTypesMap.get(queueProcessingStrategyTypes[strategy]).label | translate }} - + } +
@@ -95,55 +111,73 @@ queue.retries - - {{ 'queue.retries-required' | translate }} - - - {{ 'queue.retries-min-value' | translate }} - + @if (queueFormGroup.get('processingStrategy.retries').hasError('required')) { + + {{ 'queue.retries-required' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.retries').hasError('min') && + !queueFormGroup.get('processingStrategy.retries').hasError('required')) { + + {{ 'queue.retries-min-value' | translate }} + + } queue.failure-percentage - - {{ 'queue.failure-percentage-required' | translate }} - - - {{ 'queue.failure-percentage-min-value' | translate }} - - - {{ 'queue.failure-percentage-max-value' | translate }} - + @if (queueFormGroup.get('processingStrategy.failurePercentage').hasError('required')) { + + {{ 'queue.failure-percentage-required' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.failurePercentage').hasError('min') && + !queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') && + !queueFormGroup.get('processingStrategy.failurePercentage').hasError('max')) { + + {{ 'queue.failure-percentage-min-value' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.failurePercentage').hasError('max') && + !queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') && + !queueFormGroup.get('processingStrategy.failurePercentage').hasError('min')) { + + {{ 'queue.failure-percentage-max-value' | translate }} + + } queue.pause-between-retries - - {{ 'queue.pause-between-retries-required' | translate }} - - - {{ 'queue.pause-between-retries-min-value' | translate }} - + @if (queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')) { + + {{ 'queue.pause-between-retries-required' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('min') && + !queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')) { + + {{ 'queue.pause-between-retries-min-value' | translate }} + + } queue.max-pause-between-retries - - {{ 'queue.max-pause-between-retries-required' | translate }} - - - {{ 'queue.max-pause-between-retries-min-value' | translate }} - + @if (queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')) { + + {{ 'queue.max-pause-between-retries-required' | translate }} + + } + @if (queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('min') && + !queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')) { + + {{ 'queue.max-pause-between-retries-min-value' | translate }} + + }
@@ -163,24 +197,32 @@ queue.poll-interval - - {{ 'queue.poll-interval-required' | translate }} - - - {{ 'queue.poll-interval-min-value' | translate }} - + @if (queueFormGroup.get('pollInterval').hasError('required')) { + + {{ 'queue.poll-interval-required' | translate }} + + } + @if (queueFormGroup.get('pollInterval').hasError('min') && + !queueFormGroup.get('pollInterval').hasError('required')) { + + {{ 'queue.poll-interval-min-value' | translate }} + + } queue.partitions - - {{ 'queue.partitions-required' | translate }} - - - {{ 'queue.partitions-min-value' | translate }} - + @if (queueFormGroup.get('partitions').hasError('required')) { + + {{ 'queue.partitions-required' | translate }} + + } + @if (queueFormGroup.get('partitions').hasError('min') && + !queueFormGroup.get('partitions').hasError('required')) { + + {{ 'queue.partitions-min-value' | translate }} + + } @@ -193,13 +235,17 @@ queue.processing-timeout - - {{ 'queue.pack-processing-timeout-required' | translate }} - - - {{ 'queue.pack-processing-timeout-min-value' | translate }} - + @if (queueFormGroup.get('packProcessingTimeout').hasError('required')) { + + {{ 'queue.pack-processing-timeout-required' | translate }} + + } + @if (queueFormGroup.get('packProcessingTimeout').hasError('min') && + !queueFormGroup.get('packProcessingTimeout').hasError('required')) { + + {{ 'queue.pack-processing-timeout-min-value' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html index 6b63c75a35..1bb6e97703 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
{{(direction === entitySearchDirection.FROM ? - 'relation.to-entity' : 'relation.from-entity') | translate}} + 'relation.to-entity' : 'relation.from-entity') | translate}} - + @if (submitted && + additionalInfo.invalid) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html index 99444c2d2f..71166057d5 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html @@ -18,40 +18,46 @@
-
+ @if (enableNotOption) { +
+ }
{{ 'relation.relation-type' | translate }}
{{ 'entity.entity-types' | translate }}
-
- - {{ 'relation.not' | translate }} - - - - - -
- + @for (relationFilterControl of relationFiltersFormArray.controls; track relationFilterControl) { +
+ @if (enableNotOption) { + + {{ 'relation.not' | translate }} + + } + + + + +
+ +
-
+ }
relation.any-relation
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index 5335f36ff4..3d6e96f3b6 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -27,9 +27,11 @@ relation.direction - - {{ directionTypeTranslations.get(directions[type]) | translate }} - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(directions[type]) | translate }} + + }
@@ -87,7 +89,7 @@
+ matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> + > - - {{ 'relation.to-entity-type' | translate }} - - {{ relation.toEntityTypeName }} - - - - {{ 'relation.to-entity-name' | translate }} - + @if (direction === directions.FROM) { + + {{ 'relation.to-entity-type' | translate }} + + {{ relation.toEntityTypeName }} + + + } + @if (direction === directions.FROM) { + + {{ 'relation.to-entity-name' | translate }} + @if(relation.entityURL){ - {{ relation.toName | customTranslate }} + {{ relation.toName | customTranslate }} } @else { {{ relation.toName | customTranslate }} } - - - - {{ 'relation.from-entity-type' | translate }} - - {{ relation.fromEntityTypeName }} - - - - {{ 'relation.from-entity-name' | translate }} - - @if(relation.entityURL){ - + + + } + @if (direction === directions.TO) { + + {{ 'relation.from-entity-type' | translate }} + + {{ relation.fromEntityTypeName }} + + + } + @if (direction === directions.TO) { + + {{ 'relation.from-entity-name' | translate }} + + @if(relation.entityURL){ + + {{ relation.fromName | customTranslate }} + + } @else { {{ relation.fromName | customTranslate }} - - } @else { - {{ relation.fromName | customTranslate }} - } - - + } + + + } @@ -179,16 +189,16 @@ + *matRowDef="let relation; columns: displayedColumns;" (click)="dataSource.selection.toggle(relation)">
{{ 'relation.no-relations-text' }} + class="no-data-found flex items-center justify-center" translate>{{ 'relation.no-relations-text' }}
+ [hidePageSize]="hidePageSize">
diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html index fa7efc278a..7ab487e78c 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
- + @if (!routerOutlet?.activatedRouteData?.hideTabs && !hideCurrentTabs && (tabs$ | async).length > 1) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html index 19c65c67f2..c753c9cb11 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html @@ -24,41 +24,52 @@ [required]="required" [matAutocomplete]="ruleChainAutocomplete" [class.!hidden]="disabled && selectRuleChainFormGroup.get('ruleChainId').value"> - - {{ displayRuleChainFn(selectRuleChainFormGroup.get('ruleChainId').value) }} - - + @if (selectRuleChainFormGroup.get('ruleChainId').value && disabled) { + + {{ displayRuleChainFn(selectRuleChainFormGroup.get('ruleChainId').value) }} + + } + @if (selectRuleChainFormGroup.get('ruleChainId').value && !disabled) { + + } - - - - -
-
- rulechain.no-rulechains-text -
- - + @for (ruleChain of filteredRuleChains | async; track ruleChain) { + + + + } + @if (!(filteredRuleChains | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ rulechain.no-rulechains-text +
+ } @else { + {{ translate.get('rulechain.no-rulechains-matching', - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + } + + rulechain.create-new-rulechain - - - rulechain.create-new-rulechain - -
-
+
+
+ }
- - {{ 'rulechain.rulechain-required' | translate }} - + @if (selectRuleChainFormGroup.get('ruleChainId').hasError('required')) { + + {{ 'rulechain.rulechain-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html index 9f51ca5189..5b403ff611 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html @@ -19,26 +19,36 @@ + > - - - - - + @if (timeseries) { + + } + @if (attributes) { + + } + @if (latest) { + + } + @if (webSockets) { + + } + @if (calculatedFields) { + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html index b50241b5cd..2ac22c6284 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html @@ -20,10 +20,12 @@ rule-node-config.customer-name-pattern - - {{ 'rule-node-config.customer-name-pattern-required' | translate }} - + @if (assignCustomerConfigForm.get('customerNamePattern').hasError('required') || + assignCustomerConfigForm.get('customerNamePattern').hasError('pattern')) { + + {{ 'rule-node-config.customer-name-pattern-required' | translate }} + + } rule-node-config.customer-name-pattern-hint
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html index ef4c5ee8c2..50444e3a87 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html @@ -67,9 +67,11 @@ {{ 'rule-node-config.attributes-scope' | translate }} - - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + } @@ -98,25 +100,27 @@
+ class="tb-form-row no-border no-padding"> {{ 'rule-node-config.update-attributes-only-on-value-change' | translate }}
-
- - {{ 'rule-node-config.send-attributes-updated-notification' | translate }} - -
-
- - {{ 'rule-node-config.notify-device' | translate }} - -
+ @if (attributesConfigForm.get('scope').value !== attributeScopeMap.CLIENT_SCOPE) { +
+ + {{ 'rule-node-config.send-attributes-updated-notification' | translate }} + +
+ } + @if (attributesConfigForm.get('scope').value === attributeScopeMap.SHARED_SCOPE) { +
+ + {{ 'rule-node-config.notify-device' | translate }} + +
+ } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts index ab43627104..5a3d03ee83 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts @@ -41,7 +41,7 @@ import { export class AttributesConfigComponent extends RuleNodeConfigurationComponent { attributeScopeMap = AttributeScope; - attributeScopes = Object.keys(AttributeScope); + attributeScopes = Object.values(AttributeScope); telemetryTypeTranslationsMap = telemetryTypeTranslations; ProcessingType = ProcessingType; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.html index 328554136a..4248d39f6e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/clear-alarm-config.component.html @@ -16,41 +16,47 @@ -->
- - - - - - - + @if (tbelEnabled) { + + } + @if (clearAlarmConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (clearAlarmConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html index f5e933a6d5..eba427c385 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html @@ -19,110 +19,137 @@ {{ 'rule-node-config.use-message-alarm-data' | translate }} - - {{ 'rule-node-config.overwrite-alarm-details' | translate }} - -
- - - - - - - -
- -
-
-
- - rule-node-config.alarm-type - - - {{ 'rule-node-config.alarm-type-required' | translate }} - - rule-node-config.general-pattern-hint - - - {{ 'rule-node-config.use-alarm-severity-pattern' | translate }} - - - rule-node-config.alarm-severity - - - {{ alarmSeverityTranslationMap.get(severity) | translate }} - - - - {{ 'rule-node-config.alarm-severity-required' | translate }} - - - - rule-node-config.alarm-severity-pattern - - - {{ 'rule-node-config.alarm-severity-required' | translate }} - - - - - {{ 'rule-node-config.propagate' | translate }} + @if (createAlarmConfigForm.get('useMessageAlarmData').value === true) { + + {{ 'rule-node-config.overwrite-alarm-details' | translate }} -
- - rule-node-config.relation-types-list - - - {{key}} - close - - - - rule-node-config.relation-types-list-hint + } + @if (createAlarmConfigForm.get('useMessageAlarmData').value === false || + createAlarmConfigForm.get('overwriteAlarmDetails').value === true) { +
+ @if (tbelEnabled) { + + } + @if (createAlarmConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (createAlarmConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + } +
+ +
+
+ } + @if (createAlarmConfigForm.get('useMessageAlarmData').value === false) { +
+ + rule-node-config.alarm-type + + @if (createAlarmConfigForm.get('alarmType').hasError('required')) { + + {{ 'rule-node-config.alarm-type-required' | translate }} + + } + rule-node-config.general-pattern-hint + + {{ 'rule-node-config.use-alarm-severity-pattern' | translate }} + + @if (!createAlarmConfigForm.get('dynamicSeverity').value) { + + rule-node-config.alarm-severity + + @for (severity of alarmSeverities; track severity) { + + {{ alarmSeverityTranslationMap.get(severity) | translate }} + + } + + @if (createAlarmConfigForm.get('severity').hasError('required')) { + + {{ 'rule-node-config.alarm-severity-required' | translate }} + + } + + } + @if (createAlarmConfigForm.get('dynamicSeverity').value) { + + rule-node-config.alarm-severity-pattern + + @if (createAlarmConfigForm.get('severity').hasError('required')) { + + {{ 'rule-node-config.alarm-severity-required' | translate }} + + } + + + } + + {{ 'rule-node-config.propagate' | translate }} + + @if (createAlarmConfigForm.get('propagate').value === true) { +
+ + rule-node-config.relation-types-list + + @for (key of createAlarmConfigForm.get('relationTypes').value; track key) { + + {{key}} + close + + } + + + rule-node-config.relation-types-list-hint + +
+ } + + {{ 'rule-node-config.propagate-to-owner' | translate }} + + + {{ 'rule-node-config.propagate-to-tenant' | translate }} +
- - {{ 'rule-node-config.propagate-to-owner' | translate }} - - - {{ 'rule-node-config.propagate-to-tenant' | translate }} - -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts index cd2122a8dd..06cf94b74d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.ts @@ -42,7 +42,7 @@ export class CreateAlarmConfigComponent extends RuleNodeConfigurationComponent { @ViewChild('jsFuncComponent', {static: false}) jsFuncComponent: JsFuncComponent; @ViewChild('tbelFuncComponent', {static: false}) tbelFuncComponent: JsFuncComponent; - alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverities = Object.values(AlarmSeverity); alarmSeverityTranslationMap = alarmSeverityTranslations; createAlarmConfigForm: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html index b7b72ef390..817ff84b53 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html @@ -22,9 +22,11 @@ relation.direction - - {{ directionTypeTranslations.get(type) | translate }} - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(type) | translate }} + + } - - {{ entityTypeNamePatternTranslation.get(createRelationConfigForm.get('entityType').value) | translate }} - - + @if (createRelationConfigForm.get('entityType').value && + createRelationConfigForm.get('entityType').value !== entityType.TENANT) { + + {{ entityTypeNamePatternTranslation.get(createRelationConfigForm.get('entityType').value) | translate }} + + + } - - rule-node-config.profile-name - - + @if (createRelationConfigForm.get('entityType').value === entityType.DEVICE || + createRelationConfigForm.get('entityType').value === entityType.ASSET) { + + rule-node-config.profile-name + + + }
- + @if (createRelationConfigForm.get('entityType').value === entityType.CUSTOMER || + createRelationConfigForm.get('entityType').value === entityType.DEVICE || + createRelationConfigForm.get('entityType').value === entityType.ASSET) { + + } -
- - {{ 'rule-node-config.create-entity-if-not-exists' | translate }} - -
+ @if (createRelationConfigForm.get('entityType').value === entityType.CUSTOMER || + createRelationConfigForm.get('entityType').value === entityType.DEVICE || + createRelationConfigForm.get('entityType').value === entityType.ASSET) { +
+ + {{ 'rule-node-config.create-entity-if-not-exists' | translate }} + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts index 5d3e85e565..89e2e3be5d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts @@ -28,7 +28,7 @@ import { EntityType } from '@app/shared/models/entity-type.models'; }) export class CreateRelationConfigComponent extends RuleNodeConfigurationComponent { - directionTypes = Object.keys(EntitySearchDirection); + directionTypes = Object.values(EntitySearchDirection); directionTypeTranslations = new Map( [ [EntitySearchDirection.FROM, 'rule-node-config.search-direction-from'], @@ -105,4 +105,8 @@ export class CreateRelationConfigComponent extends RuleNodeConfigurationComponen configuration.entityTypePattern = configuration.entityTypePattern ? configuration.entityTypePattern.trim() : null; return configuration; } + + getDirectionTypeTranslations(type: string) { + return this.directionTypeTranslations.get(type as EntitySearchDirection); + } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html index 165510c657..cb0b89d1ed 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html @@ -24,9 +24,11 @@ {{ 'rule-node-config.attributes-scope' | translate }} - - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + } @@ -50,19 +52,22 @@ {{ 'rule-node-config.attributes-keys' | translate }} - - {{key}} - close - + @for (key of deleteAttributesConfigForm.get('keys').value; track key) { + + {{key}} + close + + } - {{ 'rule-node-config.attributes-keys-required' | translate }} + @if (deleteAttributesConfigForm.get('keys').hasError('required')) { + {{ 'rule-node-config.attributes-keys-required' | translate }} + } rule-node-config.general-pattern-hint @@ -77,13 +82,14 @@ {{ 'rule-node-config.send-attributes-deleted-notification' | translate }}
-
- - {{ 'rule-node-config.notify-device' | translate }} - -
+ @if (deleteAttributesConfigForm.get('scope').value === attributeScopeMap.SHARED_SCOPE) { +
+ + {{ 'rule-node-config.notify-device' | translate }} + +
+ } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts index 565b5e9dea..90818f9ceb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.ts @@ -33,7 +33,7 @@ export class DeleteAttributesConfigComponent extends RuleNodeConfigurationCompon deleteAttributesConfigForm: UntypedFormGroup; attributeScopeMap = AttributeScope; - attributeScopes = Object.keys(AttributeScope); + attributeScopes = Object.values(AttributeScope); telemetryTypeTranslationsMap = telemetryTypeTranslations; separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html index dd6c2fab93..7899e4773d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html @@ -22,9 +22,11 @@ relation.direction - - {{ directionTypeTranslations.get(type) | translate }} - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(type) | translate }} + + }
-
-
- - - - {{ entityTypeNamePatternTranslation.get(deleteRelationConfigForm.get('entityType').value) | translate }} - - + @if (deleteRelationConfigForm.get('deleteForSingleEntity').value) { +
+
+ + + @if (deleteRelationConfigForm.get('entityType').value && deleteRelationConfigForm.get('entityType').value !== entityType.TENANT) { + + {{ entityTypeNamePatternTranslation.get(deleteRelationConfigForm.get('entityType').value) | translate }} + + + } +
+ @if (deleteRelationConfigForm.get('entityType').value && deleteRelationConfigForm.get('entityType').value !== entityType.TENANT) { + + }
- -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts index 606035bd08..b32dc1f157 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts @@ -28,7 +28,7 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@app/shar }) export class DeleteRelationConfigComponent extends RuleNodeConfigurationComponent { - directionTypes = Object.keys(EntitySearchDirection); + directionTypes = Object.values(EntitySearchDirection); directionTypeTranslations = new Map( [ @@ -99,4 +99,8 @@ export class DeleteRelationConfigComponent extends RuleNodeConfigurationComponen configuration.entityNamePattern = configuration.entityNamePattern ? configuration.entityNamePattern.trim() : null; return configuration; } + + getDirectionTypeTranslations(type: string) { + return this.directionTypeTranslations.get(type as EntitySearchDirection); + } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html index c2e9a00a6e..cb3d6d2b7e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html @@ -16,12 +16,14 @@ -->
- - {{ 'rule-node-config.select-device-connectivity-event' | translate }} - - - {{ messageTypeNames.get(eventOption) }} - - - + + {{ 'rule-node-config.select-device-connectivity-event' | translate }} + + @for (eventOption of eventOptions; track eventOption) { + + {{ messageTypeNames.get(eventOption) }} + + } + +
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html index 4463973874..cea850d0be 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html @@ -22,22 +22,30 @@ rule-node-config.message-count - - {{ 'rule-node-config.message-count-required' | translate }} - - - {{ 'rule-node-config.min-message-count-message' | translate }} - + @if (generatorConfigForm.get('msgCount').hasError('required')) { + + {{ 'rule-node-config.message-count-required' | translate }} + + } + @if (generatorConfigForm.get('msgCount').hasError('min')) { + + {{ 'rule-node-config.min-message-count-message' | translate }} + + } rule-node-config.generation-frequency-seconds - - {{ 'rule-node-config.generation-frequency-required' | translate }} - - - {{ 'rule-node-config.min-generation-frequency-message' | translate }} - + @if (generatorConfigForm.get('periodInSeconds').hasError('required')) { + + {{ 'rule-node-config.generation-frequency-required' | translate }} + + } + @if (generatorConfigForm.get('periodInSeconds').hasError('min')) { + + {{ 'rule-node-config.min-generation-frequency-message' | translate }} + + }
@@ -57,56 +65,62 @@ rule-node-config.generator-function - - - - {{ 'rule-node-config.script-lang-tbel' | translate }} - - - {{ 'rule-node-config.script-lang-js' | translate }} - - - - - - - - {{ 'rule-node-config.script-lang-tbel' | translate }} - - - {{ 'rule-node-config.script-lang-js' | translate }} - - - - + @if (generatorConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + @if (tbelEnabled) { + + + {{ 'rule-node-config.script-lang-tbel' | translate }} + + + {{ 'rule-node-config.script-lang-js' | translate }} + + + } + + + } + @if (generatorConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + {{ 'rule-node-config.script-lang-tbel' | translate }} + + + {{ 'rule-node-config.script-lang-js' | translate }} + + + + + }
rule-node-config.coordinate-field-hint
@@ -44,9 +48,11 @@ {{ 'rule-node-config.perimeter-type' | translate }} - - {{ perimeterTypeTranslationMap.get(type) | translate }} - + @for (type of perimeterTypes; track type) { + + {{ perimeterTypeTranslationMap.get(type) | translate }} + + }
- {{ 'rule-node-config.perimeter-key-name' | translate }} - - - {{ 'rule-node-config.perimeter-key-name-required' | translate }} - - {{ 'rule-node-config.perimeter-key-name-hint' | translate }} - -
-
- - {{ 'rule-node-config.circle-center-latitude' | translate }} - - - {{ 'rule-node-config.circle-center-latitude-required' | translate }} + @if (geoActionConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { + + {{ 'rule-node-config.perimeter-key-name' | translate }} + + @if (geoActionConfigForm.get('perimeterKeyName').hasError('required')) { + + {{ 'rule-node-config.perimeter-key-name-required' | translate }} - - - {{ 'rule-node-config.circle-center-longitude' | translate }} - - - {{ 'rule-node-config.circle-center-longitude-required' | translate }} - - + } + {{ 'rule-node-config.perimeter-key-name-hint' | translate }} + + } + @if (geoActionConfigForm.get('perimeterType').value === perimeterType.CIRCLE && + !geoActionConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { +
+
+ + {{ 'rule-node-config.circle-center-latitude' | translate }} + + @if (geoActionConfigForm.get('centerLatitude').hasError('required')) { + + {{ 'rule-node-config.circle-center-latitude-required' | translate }} + + } + + + {{ 'rule-node-config.circle-center-longitude' | translate }} + + @if (geoActionConfigForm.get('centerLongitude').hasError('required')) { + + {{ 'rule-node-config.circle-center-longitude-required' | translate }} + + } + +
+
+ + {{ 'rule-node-config.range' | translate }} + + @if (geoActionConfigForm.get('range').hasError('required')) { + + {{ 'rule-node-config.range-required' | translate }} + + } + + + {{ 'rule-node-config.range-units' | translate }} + + @for (type of rangeUnits; track type) { + + {{ rangeUnitTranslationMap.get(type) | translate }} + + } + + @if (geoActionConfigForm.get('rangeUnit').hasError('required')) { + + {{ 'rule-node-config.range-units-required' | translate }} + + } + +
-
- - {{ 'rule-node-config.range' | translate }} - - - {{ 'rule-node-config.range-required' | translate }} - - - - {{ 'rule-node-config.range-units' | translate }} - - - {{ rangeUnitTranslationMap.get(type) | translate }} - - - - {{ 'rule-node-config.range-units-required' | translate }} - + } + @if (geoActionConfigForm.get('perimeterType').value === perimeterType.POLYGON && + !geoActionConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { +
+ + rule-node-config.polygon-definition + + + help + + + @if (geoActionConfigForm.get('polygonsDefinition').hasError('required')) { + + {{ 'rule-node-config.polygon-definition-required' | translate }} + + }
-
-
- - rule-node-config.polygon-definition - - - help - - - - {{ 'rule-node-config.polygon-definition-required' | translate }} - - -
+ }
{{ 'rule-node-config.presence-monitoring-strategy' | translate }}
- - {{ presenceMonitoringStrategies.get(strategy).name | translate }} - + @for (strategy of presenceMonitoringStrategyKeys; track strategy) { + + {{ presenceMonitoringStrategies.get(strategy).name | translate }} + + }
{{ geoActionConfigForm.get('reportPresenceStatusOnEachMessage').value === false ? - ('rule-node-config.presence-monitoring-strategy-on-first-message-hint' | translate) : - ('rule-node-config.presence-monitoring-strategy-on-each-message-hint' | translate) }} + ('rule-node-config.presence-monitoring-strategy-on-first-message-hint' | translate) : + ('rule-node-config.presence-monitoring-strategy-on-each-message-hint' | translate) }}
-
-
- - rule-node-config.min-inside-duration - - - {{ 'rule-node-config.min-inside-duration-value-required' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - - rule-node-config.min-inside-duration-time-unit - - - {{ timeUnitsTranslationMap.get(timeUnit) | translate }} - - - -
-
- - rule-node-config.min-outside-duration - - - {{ 'rule-node-config.min-outside-duration-value-required' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - - rule-node-config.min-outside-duration-time-unit - - - {{ timeUnitsTranslationMap.get(timeUnit) | translate }} - - - + @if (geoActionConfigForm.get('reportPresenceStatusOnEachMessage').value === false) { +
+
+ + rule-node-config.min-inside-duration + + @if (geoActionConfigForm.get('minInsideDuration').hasError('required')) { + + {{ 'rule-node-config.min-inside-duration-value-required' | translate }} + + } + @if (geoActionConfigForm.get('minInsideDuration').hasError('min')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + @if (geoActionConfigForm.get('minInsideDuration').hasError('max')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + + + rule-node-config.min-inside-duration-time-unit + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitsTranslationMap.get(timeUnit) | translate }} + + } + + +
+
+ + rule-node-config.min-outside-duration + + @if (geoActionConfigForm.get('minOutsideDuration').hasError('required')) { + + {{ 'rule-node-config.min-outside-duration-value-required' | translate }} + + } + @if (geoActionConfigForm.get('minOutsideDuration').hasError('min')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + @if (geoActionConfigForm.get('minOutsideDuration').hasError('max')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + + + rule-node-config.min-outside-duration-time-unit + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitsTranslationMap.get(timeUnit) | translate }} + + } + + +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.ts index b74efbe677..fe6bb80a97 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.ts @@ -38,16 +38,16 @@ export class GpsGeoActionConfigComponent extends RuleNodeConfigurationComponent geoActionConfigForm: UntypedFormGroup; perimeterType = PerimeterType; - perimeterTypes = Object.keys(PerimeterType); + perimeterTypes = Object.values(PerimeterType); perimeterTypeTranslationMap = perimeterTypeTranslations; - rangeUnits = Object.keys(RangeUnit); + rangeUnits = Object.values(RangeUnit); rangeUnitTranslationMap = rangeUnitTranslations; presenceMonitoringStrategies = PresenceMonitoringStrategiesData; presenceMonitoringStrategyKeys = Array.from(this.presenceMonitoringStrategies.keys()); - timeUnits = Object.keys(TimeUnit); + timeUnits = Object.values(TimeUnit); timeUnitsTranslationMap = timeUnitTranslations; public defaultPaddingEnable = true; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/log-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/log-config.component.html index 0d2aad6135..3998c9b42d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/log-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/log-config.component.html @@ -16,41 +16,47 @@ -->
- - - - - - - + @if (tbelEnabled) { + + } + @if (logConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (logConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.html index 980a370cb3..31010602c3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.html @@ -19,18 +19,24 @@ rule-node-config.interval-seconds - - {{ 'rule-node-config.interval-seconds-required' | translate }} - - - {{ 'rule-node-config.min-interval-seconds-message' | translate }} - + @if (msgCountConfigForm.get('interval').hasError('required')) { + + {{ 'rule-node-config.interval-seconds-required' | translate }} + + } + @if (msgCountConfigForm.get('interval').hasError('min')) { + + {{ 'rule-node-config.min-interval-seconds-message' | translate }} + + } rule-node-config.output-timeseries-key-prefix - - {{ 'rule-node-config.output-timeseries-key-prefix-required' | translate }} - + @if (msgCountConfigForm.get('telemetryPrefix').hasError('required')) { + + {{ 'rule-node-config.output-timeseries-key-prefix-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.html index 9610a0e82a..a7dc7b7b1b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.html @@ -20,38 +20,51 @@ {{ 'rule-node-config.use-metadata-period-in-seconds-patterns' | translate }}
rule-node-config.use-metadata-period-in-seconds-patterns-hint
- - rule-node-config.period-seconds - - - {{ 'rule-node-config.period-seconds-required' | translate }} - - - {{ 'rule-node-config.min-period-0-seconds-message' | translate }} - - - + @if (msgDelayConfigForm.get('useMetadataPeriodInSecondsPatterns').value !== true) { + + rule-node-config.period-seconds + + @if (msgDelayConfigForm.get('periodInSeconds').hasError('required')) { + + {{ 'rule-node-config.period-seconds-required' | translate }} + + } + @if (msgDelayConfigForm.get('periodInSeconds').hasError('min')) { + + {{ 'rule-node-config.min-period-0-seconds-message' | translate }} + + } + + } @else { rule-node-config.period-in-seconds-pattern - - {{ 'rule-node-config.period-in-seconds-pattern-required' | translate }} - + @if (msgDelayConfigForm.get('periodInSecondsPattern').hasError('required')) { + + {{ 'rule-node-config.period-in-seconds-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint - + } rule-node-config.max-pending-messages - - {{ 'rule-node-config.max-pending-messages-required' | translate }} - - - {{ 'rule-node-config.max-pending-messages-range' | translate }} - - - {{ 'rule-node-config.max-pending-messages-range' | translate }} - + @if (msgDelayConfigForm.get('maxPendingMsgs').hasError('required')) { + + {{ 'rule-node-config.max-pending-messages-required' | translate }} + + } + @if (msgDelayConfigForm.get('maxPendingMsgs').hasError('min')) { + + {{ 'rule-node-config.max-pending-messages-range' | translate }} + + } + @if (msgDelayConfigForm.get('maxPendingMsgs').hasError('max')) { + + {{ 'rule-node-config.max-pending-messages-range' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.html index a52e015a8b..5a39dae562 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.html @@ -24,9 +24,11 @@ {{ 'rule-node-config.attributes-scope' | translate }} - - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.ts index a7a5542b98..fccc665210 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.ts @@ -27,7 +27,7 @@ import { AttributeScope, telemetryTypeTranslations } from '@shared/models/teleme }) export class PushToCloudConfigComponent extends RuleNodeConfigurationComponent { - attributeScopes = Object.keys(AttributeScope); + attributeScopes = Object.values(AttributeScope); telemetryTypeTranslationsMap = telemetryTypeTranslations; pushToCloudConfigForm: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.html index 128dbf5ef3..4f7f19bbbf 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.html @@ -24,9 +24,11 @@ {{ 'rule-node-config.attributes-scope' | translate }} - - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.ts index 5192161cc8..872e7a1f46 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-edge-config.component.ts @@ -27,7 +27,7 @@ import { AttributeScope, telemetryTypeTranslations } from '@shared/models/teleme }) export class PushToEdgeConfigComponent extends RuleNodeConfigurationComponent { - attributeScopes = Object.keys(AttributeScope); + attributeScopes = Object.values(AttributeScope); telemetryTypeTranslationsMap = telemetryTypeTranslations; pushToEdgeConfigForm: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-request-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-request-config.component.html index 7394f04eff..053668a773 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-request-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/rpc-request-config.component.html @@ -19,11 +19,15 @@ rule-node-config.timeout-sec - - {{ 'rule-node-config.timeout-required' | translate }} - - - {{ 'rule-node-config.min-timeout-message' | translate }} - + @if (rpcRequestConfigForm.get('timeoutInSeconds').hasError('required')) { + + {{ 'rule-node-config.timeout-required' | translate }} + + } + @if (rpcRequestConfigForm.get('timeoutInSeconds').hasError('min')) { + + {{ 'rule-node-config.min-timeout-message' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html index 86fe2c0c84..5302d2233b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html @@ -27,10 +27,12 @@ matTooltip="{{ 'rule-node-config.custom-table-hint' | translate }}"> help - - {{ 'rule-node-config.custom-table-name-required' | translate }} - + @if (saveToCustomTableConfigForm.get('tableName').hasError('required') || + saveToCustomTableConfigForm.get('tableName').hasError('pattern')) { + + {{ 'rule-node-config.custom-table-name-required' | translate }} + + } rule-node-config.default-ttl rule-node-config.default-ttl-zero-hint - - {{ 'rule-node-config.min-default-ttl-message' | translate }} - - - {{ 'rule-node-config.default-ttl-required' | translate }} - + @if (saveToCustomTableConfigForm.get('defaultTtl').hasError('min')) { + + {{ 'rule-node-config.min-default-ttl-message' | translate }} + + } + @if (saveToCustomTableConfigForm.get('defaultTtl').hasError('required')) { + + {{ 'rule-node-config.default-ttl-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/unassign-customer-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/unassign-customer-config.component.html index e20dc783cc..91a0ff8dc9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/unassign-customer-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/unassign-customer-config.component.html @@ -25,15 +25,19 @@ {{ 'rule-node-config.unassign-from-customer' | translate }}
- - rule-node-config.customer-name-pattern - - - {{ 'rule-node-config.customer-name-pattern-required' | translate }} - - rule-node-config.customer-name-pattern-hint - + @if (unassignCustomerConfigForm.get('unassignFromCustomer').value) { + + rule-node-config.customer-name-pattern + + @if (unassignCustomerConfigForm.get('customerNamePattern').hasError('required') || + unassignCustomerConfigForm.get('customerNamePattern').hasError('pattern')) { + + {{ 'rule-node-config.customer-name-pattern-required' | translate }} + + } + rule-node-config.customer-name-pattern-hint + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html index 9250c5b544..ee56981b6a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html @@ -23,101 +23,131 @@ [formGroup]="argumentsFormGroup" (cdkDropListDropped)="onDrop($event)" [cdkDropListDisabled]="disabled"> - -
- -
- {{argumentControl.get('name').value}}. -
- - rule-node-config.argument-source-field-input - - - {{ argumentTypeMap.get(argumentControl.get('type').value)?.name | translate }} - - - {{ argumentTypeMap.get(argument).name | translate }} - - {{ argumentTypeMap.get(argument).description | translate }} - - - - - rule-node-config.argument-source-field-input-required - - -
- - rule-node-config.argument-key-field-input - - - help - - - rule-node-config.argument-key-field-input-required - - - - rule-node-config.constant-value-field-input - - - rule-node-config.constant-value-field-input-required - - - - rule-node-config.default-value-field-input - + @for (argumentControl of argumentsFormArray.controls; track argumentControl) { + +
+ @if (!disabled) { + + } +
+ @if (displayArgumentName) { + {{argumentControl.get('name').value}}. + } +
+ + rule-node-config.argument-source-field-input + + + {{ argumentTypeMap.get(argumentControl.get('type').value)?.name | translate }} + + @for (argument of arguments; track argument) { + + {{ argumentTypeMap.get(argument).name | translate }} + + {{ argumentTypeMap.get(argument).description | translate }} + + + } + + @if (argumentControl.get('type').hasError('required')) { + + rule-node-config.argument-source-field-input-required + + } +
+ @if (argumentControl.get('type').value && argumentControl.get('type').value !== ArgumentType.CONSTANT) { + + rule-node-config.argument-key-field-input + + + help + + @if (argumentControl.get('key').hasError('required')) { + + rule-node-config.argument-key-field-input-required + + } + + } + @if (argumentControl.get('type').value === ArgumentType.CONSTANT) { + + rule-node-config.constant-value-field-input + + @if (argumentControl.get('key').hasError('required')) { + + rule-node-config.constant-value-field-input-required + + } + + } + @if (argumentControl.get('type').value && argumentControl.get('type').value !== ArgumentType.CONSTANT) { + + rule-node-config.default-value-field-input + + + } +
+ @if (argumentControl.get('type').value === ArgumentType.ATTRIBUTE) { + + rule-node-config.attribute-scope-field-input + + @for (scope of attributeScope; track scope) { + + {{ attributeScopeMap.get(scope) | translate }} + + } + + @if (argumentControl.get('attributeScope').hasError('required')) { + + rule-node-config.attribute-scope-field-input-required + + } + + }
- - rule-node-config.attribute-scope-field-input - - - {{ attributeScopeMap.get(scope) | translate }} - - - - rule-node-config.attribute-scope-field-input-required - - + @if (!disabled) { + + }
-
-
- + + }
-
+ @if (!argumentsFormArray.length) { +
rule-node-config.no-arguments-prompt -
+ class="tb-prompt flex items-center justify-center">rule-node-config.no-arguments-prompt +
+ }
-
- - {{ 'alias.last-level-relation' | translate }} - -
+ @if (deviceRelationsQueryFormGroup.get('maxLevel').value > 1) { +
+ + {{ 'alias.last-level-relation' | translate }} + +
+ } @@ -61,6 +68,6 @@ [filledInputPlaceholder]="'rule-node-config.add-device-profile' | translate" formControlName="deviceTypes"> help + matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.device-profile' | translate } }}">help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.html index a54fa0d8b9..c332677027 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.html @@ -18,12 +18,14 @@
-
-
+ class="w-full">
+ @if (popupHelpLink) { +
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html index a0ce66a5a7..aed36fb6a2 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html @@ -22,34 +22,42 @@
-
- - - - {{ keyRequiredText | translate }} - - - - - - {{ valRequiredText | translate }} - - - -
-
+ @for (keyValControl of keyValsFormArray().controls; track keyValControl) { +
+ + + @if (keyValControl.get('key').hasError('required')) { + + {{ keyRequiredText | translate }} + + } + + + + @if (keyValControl.get('value').hasError('required')) { + + {{ valRequiredText | translate }} + + } + + +
+ } + @if (hintText) { +
+ }
{{ labelText }}
-
- {{ requiredText }} -
-
- rule-node-config.map-fields-required -
-
- {{ 'rule-node-config.key-val.unique-key-value-pair-error' | translate: - { - valText: valText, - keyText: keyText - } }} -
+ @if (kvListFormGroup.hasError('kvMapRequired')) { +
+ {{ requiredText }} +
+ } + @if (kvListFormGroup.hasError('required')) { +
+ rule-node-config.map-fields-required +
+ } + @if (kvListFormGroup.hasError('uniqueKeyValuePair')) { +
+ {{ 'rule-node-config.key-val.unique-key-value-pair-error' | translate: + { + valText: valText, + keyText: keyText + } }} +
+ }
@@ -40,26 +46,28 @@
-
- - - - - - -
- + @for (keyValControl of keyValsFormArray().controls; track keyValControl) { +
+ + + + + + +
+ +
-
+ }
@@ -68,5 +76,7 @@ {{ 'rule-node-config.key-val.add-mapping-entry' | translate }} - + @if (popupHelpLink || hintText) { + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.html index c342149422..8dfbbaf1d6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/math-function-autocomplete.component.html @@ -23,21 +23,27 @@ #operationInput matInput formControlName="operation" [matAutocomplete]="auto"> - + @if (mathFunctionForm.get('operation').value) { + + } - - - - {{ option.description }} - - - - rule-node-config.no-option-found - + @for (option of filteredOptions | async; track option) { + + + + {{ option.description }} + + + } + @if (!(filteredOptions | async)?.length) { + + rule-node-config.no-option-found + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html index 7ad685d8e2..dbd1ab1e29 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html @@ -16,15 +16,18 @@ --> - {{ label }} + @if (label) { + {{ label }} + } - - {{messageType.name}} - close - + @for (messageType of messageTypes; track messageType) { + + {{messageType.name}} + close + + } - - - - -
-
- rule-node-config.no-message-types-found + @for (messageType of filteredMessageTypes | async; track messageType) { + + + + } + @if ((filteredMessageTypes | async)?.length === 0) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ rule-node-config.no-message-types-found +
+ } @else { + + {{ 'rule-node-config.no-message-type-matching' | translate : + {messageType: truncate.transform(searchText, true, 6, '...')} + }} + + } + + rule-node-config.create-new-message-type +
- - - {{ 'rule-node-config.no-message-type-matching' | translate : - {messageType: truncate.transform(searchText, true, 6, '...')} - }} - - - - rule-node-config.create-new-message-type - -
- + + } help - - {{ 'rule-node-config.select-message-types-required' | translate }} - + matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.message-type' | translate } }}">help + @if (chipList.errorState) { + + {{ 'rule-node-config.select-message-types-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.html index 9e7aba378f..14dfd216f5 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/msg-metadata-chip.component.html @@ -18,9 +18,11 @@
{{ labelText }}
- {{ option.name }} + @for (option of selectOptions; track option) { + {{ option.name }} + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html index 412f8af911..138d45700b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html @@ -19,32 +19,39 @@ {{'rule-node-config.output-message-type' | translate}} - - {{msgType.name}} - + @for (msgType of messageTypes; track msgType) { + + {{msgType.name}} + + } {{'rule-node-config.message-type-value' | translate}} - - - {{ 'rule-node-config.message-type-value-required' | translate }} - - - {{ 'rule-node-config.message-type-value-max-length' | translate }} - + @if (messageTypeFormGroup.get('messageType').value) { + + } + @if (messageTypeFormGroup.get('messageType').hasError('required')) { + + {{ 'rule-node-config.message-type-value-required' | translate }} + + } + @if (messageTypeFormGroup.get('messageType').hasError('maxlength')) { + + {{ 'rule-node-config.message-type-value-max-length' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.html index bab5638bd6..e6fbcdc3df 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.html @@ -23,9 +23,11 @@ relation.direction - - {{ directionTypeTranslations.get(type) | translate }} - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(type) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts index 83ed4a435f..b0adb57e2a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config-old.component.ts @@ -49,7 +49,7 @@ export class RelationsQueryConfigOldComponent extends PageComponent implements C this.requiredValue = coerceBooleanProperty(value); } - directionTypes = Object.keys(EntitySearchDirection); + directionTypes = Object.values(EntitySearchDirection); directionTypeTranslations = entitySearchDirectionTranslations; relationsQueryFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html index 936a9cde9d..5f8754c723 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html @@ -22,9 +22,11 @@ relation.direction - - {{ directionTypeTranslations.get(type) | translate }} rule-node-config.relations-query-config-direction-suffix - + @for (type of directionTypes; track type) { + + {{ directionTypeTranslations.get(type) | translate }} rule-node-config.relations-query-config-direction-suffix + + } @@ -36,21 +38,26 @@ inputmode="numeric" placeholder="{{ 'rule-node-config.unlimited-level' | translate }}" formControlName="maxLevel"> - - {{ 'rule-node-config.max-relation-level-error' | translate }} - - - {{ 'rule-node-config.max-relation-level-invalid' | translate }} - + @if (relationsQueryFormGroup.get('maxLevel').hasError('min')) { + + {{ 'rule-node-config.max-relation-level-error' | translate }} + + } + @if (relationsQueryFormGroup.get('maxLevel').invalid) { + + {{ 'rule-node-config.max-relation-level-invalid' | translate }} + + }
-
- - {{ 'alias.last-level-relation' | translate }} - -
+ @if (relationsQueryFormGroup.get('maxLevel').value > 1) { +
+ + {{ 'alias.last-level-relation' | translate }} + +
+ }
relation.relation-filters
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html index 9665fff5ce..3d93ab0b11 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html @@ -17,7 +17,7 @@ -->
+ [popupHelpLink]="popupHelpLink"> -
- - {{ 'rule-node-config.fetch-latest-telemetry-with-timestamp' | translate }} - -
+ class="tb-form-row no-border no-padding"> + + {{ 'rule-node-config.fetch-latest-telemetry-with-timestamp' | translate }} + +
+ } help + matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.field-name' | translate } }}">help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.html index b8d804bfea..dddbcac29e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/sv-map-config.component.html @@ -18,12 +18,16 @@
{{ labelText }}
-
- rule-node-config.map-fields-required -
-
- {{ requiredText }} -
+ @if (svListFormGroup.hasError('required')) { +
+ rule-node-config.map-fields-required +
+ } + @if (svListFormGroup.hasError('svMapRequired')) { +
+ {{ requiredText }} +
+ }
@@ -33,30 +37,34 @@
-
- - - - {{option.name}} - - - - - - -
- + @for (keyValControl of keyValsFormArray().controls; track keyValControl) { +
+ + + @for (option of filterSelectOptions(keyValControl); track option) { + + {{option.name}} + + } + + + + + +
+ +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html index e39b97a19d..dcc5fee728 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html @@ -20,29 +20,37 @@ {{ 'rule-node-config.input-value-key' | translate }} - - {{ 'rule-node-config.input-value-key-required' | translate }} - + @if (calculateDeltaConfigForm.get('inputValueKey').hasError('required') || + calculateDeltaConfigForm.get('inputValueKey').hasError('pattern')) { + + {{ 'rule-node-config.input-value-key-required' | translate }} + + } {{ 'rule-node-config.output-value-key' | translate }} - - {{ 'rule-node-config.output-value-key-required' | translate }} - + @if (calculateDeltaConfigForm.get('outputValueKey').hasError('required') || + calculateDeltaConfigForm.get('outputValueKey').hasError('pattern')) { + + {{ 'rule-node-config.output-value-key-required' | translate }} + + } {{ 'rule-node-config.number-of-digits-after-floating-point' | translate }} - - {{ 'rule-node-config.number-of-digits-after-floating-point-range' | translate }} - - - {{ 'rule-node-config.number-of-digits-after-floating-point-range' | translate }} - + @if (calculateDeltaConfigForm.get('round').hasError('min')) { + + {{ 'rule-node-config.number-of-digits-after-floating-point-range' | translate }} + + } + @if (calculateDeltaConfigForm.get('round').hasError('max')) { + + {{ 'rule-node-config.number-of-digits-after-floating-point-range' | translate }} + + }
+ class="tb-form-row same-padding"> {{ 'rule-node-config.use-caching' | translate }} @@ -64,26 +72,30 @@ { periodValueKey: (calculateDeltaConfigForm.get('periodValueKey').valid && calculateDeltaConfigForm.get('addPeriodBetweenMsgs').value) ? calculateDeltaConfigForm.get('periodValueKey').value : 'periodInMs' } }}" - class="tb-form-row no-border no-padding"> + class="tb-form-row no-border no-padding"> {{ 'rule-node-config.add-time-difference-between-readings' | translate: { inputValueKey: calculateDeltaConfigForm.get('inputValueKey').valid ? - calculateDeltaConfigForm.get('inputValueKey').value : 'rule-node-config.input-value-key' | translate } }} + calculateDeltaConfigForm.get('inputValueKey').value : 'rule-node-config.input-value-key' | translate } }}
- - {{ 'rule-node-config.period-value-key' | translate }} - - - {{ 'rule-node-config.period-value-key-required' | translate }} - - + @if (calculateDeltaConfigForm.get('addPeriodBetweenMsgs').value) { + + {{ 'rule-node-config.period-value-key' | translate }} + + @if (calculateDeltaConfigForm.get('periodValueKey').hasError('required')) { + + {{ 'rule-node-config.period-value-key-required' | translate }} + + } + + }
+ class="tb-form-row same-padding"> {{ 'rule-node-config.exclude-zero-deltas' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.html index e32b62a92e..e26979cce6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/customer-attributes-config.component.html @@ -20,9 +20,11 @@
- - {{ data.name }} - + @for (data of fetchToData; track data) { + + {{ data.name }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/get-telemetry-from-database-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/get-telemetry-from-database-config.component.html index d28aa22624..89e8a0090e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/get-telemetry-from-database-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/get-telemetry-from-database-config.component.html @@ -26,7 +26,7 @@ tb-help-popup-placement="right" trigger-style="letter-spacing:0.25px; font-size:12px" [tb-help-popup-style]="{maxWidth: '820px'}" - trigger-text="{{ 'rule-node-config.key-val.see-examples' | translate }}">
+ trigger-text="{{ 'rule-node-config.key-val.see-examples' | translate }}"> help @@ -39,144 +39,180 @@ {{ 'rule-node-config.use-metadata-dynamic-interval' | translate }} -
-
- - {{ 'rule-node-config.interval-start' | translate }} - - - {{ 'rule-node-config.start-interval-value-required' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - - {{ 'rule-node-config.time-unit' | translate }} - - - {{ timeUnitsTranslationMap.get(timeUnit) | translate }} - - - -
-
- - {{ 'rule-node-config.interval-end' | translate }} - - - {{ 'rule-node-config.end-interval-value-required' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - {{ 'rule-node-config.time-value-range' | translate }} - - - - {{ 'rule-node-config.time-unit' | translate }} - - - {{ timeUnitsTranslationMap.get(timeUnit) | translate }} - - - -
-
- error_outline -
- - {{ 'rule-node-config.fetch-timeseries-from-to' | translate: - { + @if (getTelemetryFromDatabaseConfigForm.get('useMetadataIntervalPatterns').value === false) { +
+
+ + {{ 'rule-node-config.interval-start' | translate }} + + @if (getTelemetryFromDatabaseConfigForm.get('interval.startInterval').hasError('required')) { + + {{ 'rule-node-config.start-interval-value-required' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('interval.startInterval').hasError('min')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('interval.startInterval').hasError('max')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + + + {{ 'rule-node-config.time-unit' | translate }} + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitsTranslationMap.get(timeUnit) | translate }} + + } + + +
+
+ + {{ 'rule-node-config.interval-end' | translate }} + + @if (getTelemetryFromDatabaseConfigForm.get('interval.endInterval').hasError('required')) { + + {{ 'rule-node-config.end-interval-value-required' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('interval.endInterval').hasError('min')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('interval.endInterval').hasError('max')) { + + {{ 'rule-node-config.time-value-range' | translate }} + + } + + + {{ 'rule-node-config.time-unit' | translate }} + + @for (timeUnit of timeUnits; track timeUnit) { + + {{ timeUnitsTranslationMap.get(timeUnit) | translate }} + + } + + +
+
+ error_outline +
+ @if (getTelemetryFromDatabaseConfigForm.get('interval').valid) { + {{ 'rule-node-config.fetch-timeseries-from-to' | translate: + { startInterval: getTelemetryFromDatabaseConfigForm.get('interval.startInterval').value, endInterval: getTelemetryFromDatabaseConfigForm.get('interval.endInterval').value, startIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get('interval.startIntervalTimeUnit').value.toLowerCase(), endIntervalTimeUnit: getTelemetryFromDatabaseConfigForm.get('interval.endIntervalTimeUnit').value.toLowerCase() - } }} - - - {{ "rule-node-config.fetch-timeseries-from-to-invalid" | translate }} - + } }} + } @else { + {{ "rule-node-config.fetch-timeseries-from-to-invalid" | translate }} + } +
-
- + } @else {
{{ 'rule-node-config.start-interval' | translate }} - - {{ 'rule-node-config.start-interval-required' | translate }} - + @if (getTelemetryFromDatabaseConfigForm.get('startIntervalPattern').hasError('required') || + getTelemetryFromDatabaseConfigForm.get('startIntervalPattern').hasError('pattern')) { + + {{ 'rule-node-config.start-interval-required' | translate }} + + } {{ 'rule-node-config.end-interval' | translate }} - - {{ 'rule-node-config.end-interval-required' | translate }} - + @if (getTelemetryFromDatabaseConfigForm.get('endIntervalPattern').hasError('required') || + getTelemetryFromDatabaseConfigForm.get('endIntervalPattern').hasError('pattern')) { + + {{ 'rule-node-config.end-interval-required' | translate }} + + }
-
+ }
rule-node-config.fetch-strategy
- - {{ data.name }} - + @for (data of headerOptions; track data) { + + {{ data.name }} + + }
{{ deduplicationStrategiesHintTranslations.get(getTelemetryFromDatabaseConfigForm.get('fetchMode').value) | translate }}
-
- - {{ 'aggregation.function' | translate }} - - - {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} - - - -
- - {{ "rule-node-config.order-by-timestamp" | translate }} - - - {{ samplingOrdersTranslate.get(order) | translate }} - + @if (getTelemetryFromDatabaseConfigForm.get('fetchMode').value === fetchMode.ALL) { +
+ + {{ 'aggregation.function' | translate }} + + @for (aggregation of aggregations; track aggregation) { + + {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} + + } - - {{ "rule-node-config.limit" | translate }} - - {{ "rule-node-config.limit-hint" | translate }} - - {{ 'rule-node-config.limit-required' | translate }} - - - {{ 'rule-node-config.limit-range' | translate }} - - - {{ 'rule-node-config.limit-range' | translate }} - - + @if (getTelemetryFromDatabaseConfigForm.get('aggregation').value === aggregationTypes.NONE) { +
+ + {{ "rule-node-config.order-by-timestamp" | translate }} + + @for (order of samplingOrders; track order) { + + {{ samplingOrdersTranslate.get(order) | translate }} + + } + + + + {{ "rule-node-config.limit" | translate }} + + {{ "rule-node-config.limit-hint" | translate }} + @if (getTelemetryFromDatabaseConfigForm.get('limit').hasError('required')) { + + {{ 'rule-node-config.limit-required' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('limit').hasError('min')) { + + {{ 'rule-node-config.limit-range' | translate }} + + } + @if (getTelemetryFromDatabaseConfigForm.get('limit').hasError('max')) { + + {{ 'rule-node-config.limit-range' | translate }} + + } + +
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.html index cdce036bbb..d80674e7fb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/related-attributes-config.component.html @@ -23,9 +23,11 @@
rule-node-config.data-to-fetch
- - {{ data.name }} - + @for (data of fetchToData; track data) { + + {{ data.name }} + + }
- - {{ data.name }} - + @for (data of fetchToData; track data) { + + {{ data.name }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index 73605e0767..7167b68132 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -50,25 +50,35 @@ rule-node-config.ai.system-prompt - - {{ 'rule-node-config.ai.system-prompt-max-length' | translate }} - - - {{ 'rule-node-config.ai.system-prompt-blank' | translate }} - + @if (aiConfigForm.get('systemPrompt').hasError('maxlength')) { + + {{ 'rule-node-config.ai.system-prompt-max-length' | translate }} + + } + @if (aiConfigForm.get('systemPrompt').hasError('pattern')) { + + {{ 'rule-node-config.ai.system-prompt-blank' | translate }} + + } rule-node-config.ai.user-prompt - - {{ 'rule-node-config.ai.user-prompt-required' | translate }} - - - {{ 'rule-node-config.ai.user-prompt-max-length' | translate }} - - - {{ 'rule-node-config.ai.user-prompt-blank' | translate }} - + @if (aiConfigForm.get('userPrompt').hasError('required')) { + + {{ 'rule-node-config.ai.user-prompt-required' | translate }} + + } + @if (aiConfigForm.get('userPrompt').hasError('maxlength')) { + + {{ 'rule-node-config.ai.user-prompt-max-length' | translate }} + + } + @if (aiConfigForm.get('userPrompt').hasError('pattern')) { + + {{ 'rule-node-config.ai.user-prompt-blank' | translate }} + + } rule-node-config.topic - - {{ 'rule-node-config.topic-required' | translate }} - + @if (azureIotHubConfigForm.get('topicPattern').hasError('required')) { + + {{ 'rule-node-config.topic-required' | translate }} + + } rule-node-config.general-pattern-hint rule-node-config.hostname - - {{ 'rule-node-config.hostname-required' | translate }} - + @if (azureIotHubConfigForm.get('host').hasError('required')) { + + {{ 'rule-node-config.hostname-required' | translate }} + + } rule-node-config.device-id - - {{ 'rule-node-config.device-id-required' | translate }} - + @if (azureIotHubConfigForm.get('clientId').hasError('required')) { + + {{ 'rule-node-config.device-id-required' | translate }} + + } @@ -53,71 +59,78 @@ rule-node-config.credentials-type - - {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }} - + @for (credentialsType of allAzureIotHubCredentialsTypes; track credentialsType) { + + {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }} + + } - - {{ 'rule-node-config.credentials-type-required' | translate }} - + @if (azureIotHubConfigForm.get('credentials.type').hasError('required')) { + + {{ 'rule-node-config.credentials-type-required' | translate }} + + } -
- - - - - rule-node-config.sas-key - - - - {{ 'rule-node-config.sas-key-required' | translate }} - - - - - - - - - - - - - - rule-node-config.private-key-password - - - - +
+ @switch (azureIotHubConfigForm.get('credentials.type').value) { + @case ('anonymous') {} + @case ('sas') { + + rule-node-config.sas-key + + + @if (azureIotHubConfigForm.get('credentials.sasKey').hasError('required')) { + + {{ 'rule-node-config.sas-key-required' | translate }} + + } + + + + } + @case ('cert.PEM') { + + + + + + + + rule-node-config.private-key-password + + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html index 965a0ca8b8..ffbc99fe57 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html @@ -19,9 +19,11 @@ rule-node-config.topic-pattern - - {{ 'rule-node-config.topic-pattern-required' | translate }} - + @if (kafkaConfigForm.get('topicPattern').hasError('required')) { + + {{ 'rule-node-config.topic-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint @@ -33,44 +35,56 @@ rule-node-config.bootstrap-servers - - {{ 'rule-node-config.bootstrap-servers-required' | translate }} - + @if (kafkaConfigForm.get('bootstrapServers').hasError('required')) { + + {{ 'rule-node-config.bootstrap-servers-required' | translate }} + + } rule-node-config.retries - - {{ 'rule-node-config.min-retries-message' | translate }} - + @if (kafkaConfigForm.get('retries').hasError('min')) { + + {{ 'rule-node-config.min-retries-message' | translate }} + + } rule-node-config.batch-size-bytes - - {{ 'rule-node-config.min-batch-size-bytes-message' | translate }} - + @if (kafkaConfigForm.get('batchSize').hasError('min')) { + + {{ 'rule-node-config.min-batch-size-bytes-message' | translate }} + + } rule-node-config.linger-ms - - {{ 'rule-node-config.min-linger-ms-message' | translate }} - + @if (kafkaConfigForm.get('linger').hasError('min')) { + + {{ 'rule-node-config.min-linger-ms-message' | translate }} + + } rule-node-config.buffer-memory-bytes - - {{ 'rule-node-config.min-buffer-memory-bytes-message' | translate }} - + @if (kafkaConfigForm.get('bufferMemory').hasError('min')) { + + {{ 'rule-node-config.min-buffer-memory-bytes-message' | translate }} + + } rule-node-config.acks - - {{ ackValue }} - + @for (ackValue of ackValues; track ackValue) { + + {{ ackValue }} + + } @@ -86,12 +100,16 @@ {{ 'rule-node-config.add-metadata-key-values-as-kafka-headers' | translate }}
rule-node-config.add-metadata-key-values-as-kafka-headers-hint
- - rule-node-config.charset-encoding - - - {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }} - - - + @if (kafkaConfigForm.get('addMetadataKeyValuesAsKafkaHeaders').value) { + + rule-node-config.charset-encoding + + @for (charset of ToByteStandartCharsetTypesValues; track charset) { + + {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }} + + } + + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html index 1839464bbe..fba6b87d72 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html @@ -27,9 +27,11 @@ {{'rule-node-config.function-name' | translate}} - - {{'rule-node-config.function-name-required' | translate}} - + @if (lambdaConfigForm.get('functionName').hasError('required')) { + + {{'rule-node-config.function-name-required' | translate}} + + } {{'rule-node-config.qualifier' | translate}} @@ -48,23 +50,29 @@ rule-node-config.aws-access-key-id - - {{ 'rule-node-config.aws-access-key-id-required' | translate }} - + @if (lambdaConfigForm.get('accessKey').hasError('required')) { + + {{ 'rule-node-config.aws-access-key-id-required' | translate }} + + } rule-node-config.aws-secret-access-key - - {{ 'rule-node-config.aws-secret-access-key-required' | translate }} - + @if (lambdaConfigForm.get('secretKey').hasError('required')) { + + {{ 'rule-node-config.aws-secret-access-key-required' | translate }} + + } rule-node-config.aws-region - - {{ 'rule-node-config.aws-region-required' | translate }} - + @if (lambdaConfigForm.get('region').hasError('required')) { + + {{ 'rule-node-config.aws-region-required' | translate }} + + } @@ -79,28 +87,36 @@ rule-node-config.connection-timeout - - {{ 'rule-node-config.connection-timeout-required' | translate }} - - - {{ 'rule-node-config.connection-timeout-min' | translate }} - + @if (lambdaConfigForm.get('connectionTimeout').hasError('required')) { + + {{ 'rule-node-config.connection-timeout-required' | translate }} + + } + @if (lambdaConfigForm.get('connectionTimeout').hasError('min')) { + + {{ 'rule-node-config.connection-timeout-min' | translate }} + + } help + matTooltip="{{ 'rule-node-config.connection-timeout-hint' | translate }}">help rule-node-config.request-timeout - - {{ 'rule-node-config.request-timeout-required' | translate }} - - - {{ 'rule-node-config.request-timeout-min' | translate }} - + @if (lambdaConfigForm.get('requestTimeout').hasError('required')) { + + {{ 'rule-node-config.request-timeout-required' | translate }} + + } + @if (lambdaConfigForm.get('requestTimeout').hasError('min')) { + + {{ 'rule-node-config.request-timeout-min' | translate }} + + } help + matTooltip="{{ 'rule-node-config.request-timeout-hint' | translate }}">help
rule-node-config.topic-pattern - - {{ 'rule-node-config.topic-pattern-required' | translate }} - + @if (mqttConfigForm.get('topicPattern').hasError('required')) { + + {{ 'rule-node-config.topic-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint
rule-node-config.host - - {{ 'rule-node-config.host-required' | translate }} - + @if (mqttConfigForm.get('host').hasError('required')) { + + {{ 'rule-node-config.host-required' | translate }} + + } rule-node-config.port - - {{ 'rule-node-config.port-required' | translate }} - - - {{ 'rule-node-config.port-range' | translate }} - - - {{ 'rule-node-config.port-range' | translate }} - + @if (mqttConfigForm.get('port').hasError('required')) { + + {{ 'rule-node-config.port-required' | translate }} + + } + @if (mqttConfigForm.get('port').hasError('min')) { + + {{ 'rule-node-config.port-range' | translate }} + + } + @if (mqttConfigForm.get('port').hasError('max')) { + + {{ 'rule-node-config.port-range' | translate }} + + } rule-node-config.connect-timeout - - {{ 'rule-node-config.connect-timeout-required' | translate }} - - - {{ 'rule-node-config.connect-timeout-range' | translate }} - - - {{ 'rule-node-config.connect-timeout-range' | translate }} - + @if (mqttConfigForm.get('connectTimeoutSec').hasError('required')) { + + {{ 'rule-node-config.connect-timeout-required' | translate }} + + } + @if (mqttConfigForm.get('connectTimeoutSec').hasError('min')) { + + {{ 'rule-node-config.connect-timeout-range' | translate }} + + } + @if (mqttConfigForm.get('connectTimeoutSec').hasError('max')) { + + {{ 'rule-node-config.connect-timeout-range' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/pubsub-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/pubsub-config.component.html index 1e485288e0..68245324ba 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/pubsub-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/pubsub-config.component.html @@ -19,16 +19,20 @@ rule-node-config.gcp-project-id - - {{ 'rule-node-config.gcp-project-id-required' | translate }} - + @if (pubSubConfigForm.get('projectId').hasError('required')) { + + {{ 'rule-node-config.gcp-project-id-required' | translate }} + + } rule-node-config.pubsub-topic-name - - {{ 'rule-node-config.pubsub-topic-name-required' | translate }} - + @if (pubSubConfigForm.get('topicName').hasError('required')) { + + {{ 'rule-node-config.pubsub-topic-name-required' | translate }} + + } rule-node-config.message-properties - - {{ property }} - + @for (property of messageProperties; track property) { + + {{ property }} + + }
rule-node-config.host - - {{ 'rule-node-config.host-required' | translate }} - + @if (rabbitMqConfigForm.get('host').hasError('required')) { + + {{ 'rule-node-config.host-required' | translate }} + + } rule-node-config.port - - {{ 'rule-node-config.port-required' | translate }} - - - {{ 'rule-node-config.port-range' | translate }} - - - {{ 'rule-node-config.port-range' | translate }} - + @if (rabbitMqConfigForm.get('port').hasError('required')) { + + {{ 'rule-node-config.port-required' | translate }} + + } + @if (rabbitMqConfigForm.get('port').hasError('min')) { + + {{ 'rule-node-config.port-range' | translate }} + + } + @if (rabbitMqConfigForm.get('port').hasError('max')) { + + {{ 'rule-node-config.port-range' | translate }} + + }
@@ -73,16 +83,20 @@ rule-node-config.connection-timeout-ms - - {{ 'rule-node-config.min-connection-timeout-ms-message' | translate }} - + @if (rabbitMqConfigForm.get('connectionTimeout').hasError('min')) { + + {{ 'rule-node-config.min-connection-timeout-ms-message' | translate }} + + } rule-node-config.handshake-timeout-ms - - {{ 'rule-node-config.min-handshake-timeout-ms-message' | translate }} - + @if (rabbitMqConfigForm.get('handshakeTimeout').hasError('min')) { + + {{ 'rule-node-config.min-handshake-timeout-ms-message' | translate }} + + } rule-node-config.endpoint-url-pattern - - {{ 'rule-node-config.endpoint-url-pattern-required' | translate }} - + @if (restApiCallConfigForm.get('restEndpointUrlPattern').hasError('required')) { + + {{ 'rule-node-config.endpoint-url-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint rule-node-config.request-method - - {{ requestType }} - + @for (requestType of httpRequestTypes; track requestType) { + + {{ requestType }} + + } {{ 'rule-node-config.enable-proxy' | translate }} - - {{ 'rule-node-config.use-simple-client-http-factory' | translate }} - + @if (!restApiCallConfigForm.get('enableProxy').value) { + + {{ 'rule-node-config.use-simple-client-http-factory' | translate }} + + } {{ 'rule-node-config.parse-to-plain-text' | translate }} @@ -45,76 +51,96 @@ {{ 'rule-node-config.ignore-request-body' | translate }} -
- - {{ 'rule-node-config.use-system-proxy-properties' | translate }} - -
-
- - rule-node-config.proxy-scheme - - - {{ proxyScheme }} - - - - - rule-node-config.proxy-host - - - {{ 'rule-node-config.proxy-host-required' | translate }} - - - - rule-node-config.proxy-port - - - {{ 'rule-node-config.proxy-port-required' | translate }} - - - {{ 'rule-node-config.proxy-port-range' | translate }} - - -
- - rule-node-config.proxy-user - - - - rule-node-config.proxy-password - - + @if (restApiCallConfigForm.get('enableProxy').value) { +
+ + {{ 'rule-node-config.use-system-proxy-properties' | translate }} + + @if (!restApiCallConfigForm.get('useSystemProxyProperties').value) { +
+
+ + rule-node-config.proxy-scheme + + @for (proxyScheme of proxySchemes; track proxyScheme) { + + {{ proxyScheme }} + + } + + + + rule-node-config.proxy-host + + @if (restApiCallConfigForm.get('proxyHost').hasError('required')) { + + {{ 'rule-node-config.proxy-host-required' | translate }} + + } + + + rule-node-config.proxy-port + + @if (restApiCallConfigForm.get('proxyPort').hasError('required')) { + + {{ 'rule-node-config.proxy-port-required' | translate }} + + } + @if (restApiCallConfigForm.get('proxyPort').hasError('min') || restApiCallConfigForm.get('proxyPort').hasError('max')) { + + {{ 'rule-node-config.proxy-port-range' | translate }} + + } + +
+ + rule-node-config.proxy-user + + + + rule-node-config.proxy-password + + +
+ }
-
- - rule-node-config.read-timeout - - rule-node-config.read-timeout-hint - - {{ 'rule-node-config.int-range' | translate }} - - + } + @if (!restApiCallConfigForm.get('useSimpleClientHttpFactory').value || restApiCallConfigForm.get('enableProxy').value) { + + rule-node-config.read-timeout + + rule-node-config.read-timeout-hint + @if (restApiCallConfigForm.get('readTimeoutMs').hasError('max')) { + + {{ 'rule-node-config.int-range' | translate }} + + } + + } rule-node-config.max-parallel-requests-count rule-node-config.max-parallel-requests-count-hint - - {{ 'rule-node-config.int-range' | translate }} - + @if (restApiCallConfigForm.get('maxParallelRequestsCount').hasError('max')) { + + {{ 'rule-node-config.int-range' | translate }} + + } rule-node-config.max-response-size rule-node-config.max-response-size-hint - - {{ 'rule-node-config.memory-buffer-size-range' | translate: { max: MemoryBufferSizeInKbLimit } }} - + @if (restApiCallConfigForm.get('maxInMemoryBufferSizeInKb').hasError('min') || restApiCallConfigForm.get('maxInMemoryBufferSizeInKb').hasError('max')) { + + {{ 'rule-node-config.memory-buffer-size-range' | translate: { max: MemoryBufferSizeInKbLimit } }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html index 98e2c2f686..295de6912a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html @@ -19,98 +19,126 @@ {{ 'rule-node-config.use-system-smtp-settings' | translate }} -
- - rule-node-config.smtp-protocol - - - {{ smtpProtocol.toUpperCase() }} - - - -
- - rule-node-config.smtp-host - - - {{ 'rule-node-config.smtp-host-required' | translate }} - - - - rule-node-config.smtp-port - - - {{ 'rule-node-config.smtp-port-required' | translate }} - - - {{ 'rule-node-config.smtp-port-range' | translate }} - - - {{ 'rule-node-config.smtp-port-range' | translate }} - + @if (sendEmailConfigForm.get('useSystemSmtpSettings').value === false) { +
+ + rule-node-config.smtp-protocol + + @for (smtpProtocol of smtpProtocols; track smtpProtocol) { + + {{ smtpProtocol.toUpperCase() }} + + } + -
- - rule-node-config.timeout-msec - - - {{ 'rule-node-config.timeout-required' | translate }} - - - {{ 'rule-node-config.min-timeout-msec-message' | translate }} - - - - {{ 'rule-node-config.enable-tls' | translate }} - - - rule-node-config.tls-version - - - {{ tlsVersion }} - - - - - {{ 'rule-node-config.enable-proxy' | translate }} - -
- rule-node-config.proxy-host - - - {{ 'rule-node-config.proxy-host-required' | translate }} - + rule-node-config.smtp-host + + @if (sendEmailConfigForm.get('smtpHost').hasError('required')) { + + {{ 'rule-node-config.smtp-host-required' | translate }} + + } - rule-node-config.proxy-port - - - {{ 'rule-node-config.proxy-port-required' | translate }} - - - {{ 'rule-node-config.proxy-port-range' | translate }} - + rule-node-config.smtp-port + + @if (sendEmailConfigForm.get('smtpPort').hasError('required')) { + + {{ 'rule-node-config.smtp-port-required' | translate }} + + } + @if (sendEmailConfigForm.get('smtpPort').hasError('min')) { + + {{ 'rule-node-config.smtp-port-range' | translate }} + + } + @if (sendEmailConfigForm.get('smtpPort').hasError('max')) { + + {{ 'rule-node-config.smtp-port-range' | translate }} + + }
- rule-node-config.proxy-user - + rule-node-config.timeout-msec + + @if (sendEmailConfigForm.get('timeout').hasError('required')) { + + {{ 'rule-node-config.timeout-required' | translate }} + + } + @if (sendEmailConfigForm.get('timeout').hasError('min')) { + + {{ 'rule-node-config.min-timeout-msec-message' | translate }} + + } - - rule-node-config.proxy-password - + + {{ 'rule-node-config.enable-tls' | translate }} + + @if (sendEmailConfigForm.get('enableTls').value === true) { + + rule-node-config.tls-version + + @for (tlsVersion of tlsVersions; track tlsVersion) { + + {{ tlsVersion }} + + } + + + } + + {{ 'rule-node-config.enable-proxy' | translate }} + + @if (sendEmailConfigForm.get('enableProxy').value) { +
+
+ + rule-node-config.proxy-host + + @if (sendEmailConfigForm.get('proxyHost').hasError('required')) { + + {{ 'rule-node-config.proxy-host-required' | translate }} + + } + + + rule-node-config.proxy-port + + @if (sendEmailConfigForm.get('proxyPort').hasError('required')) { + + {{ 'rule-node-config.proxy-port-required' | translate }} + + } + @if (sendEmailConfigForm.get('proxyPort').hasError('min') || sendEmailConfigForm.get('proxyPort').hasError('max')) { + + {{ 'rule-node-config.proxy-port-range' | translate }} + + } + +
+ + rule-node-config.proxy-user + + + + rule-node-config.proxy-password + + +
+ } + + rule-node-config.username + + + + rule-node-config.password + + -
- - rule-node-config.username - - - - rule-node-config.password - - - -
+ + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html index 1f6762c082..ff9a77a3f5 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html @@ -19,25 +19,30 @@ rule-node-config.numbers-to-template - - {{ 'rule-node-config.numbers-to-template-required' | translate }} - + @if (sendSmsConfigForm.get('numbersToTemplate').hasError('required')) { + + {{ 'rule-node-config.numbers-to-template-required' | translate }} + + } rule-node-config.sms-message-template - - {{ 'rule-node-config.sms-message-template-required' | translate }} - + @if (sendSmsConfigForm.get('smsMessageTemplate').hasError('required')) { + + {{ 'rule-node-config.sms-message-template-required' | translate }} + + } rule-node-config.general-pattern-hint {{ 'rule-node-config.use-system-sms-settings' | translate }} - - + @if (sendSmsConfigForm.get('useSystemSmsSettings').value === false) { + + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html index 2cf15b05e7..9ef122ad9e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html @@ -19,26 +19,34 @@ rule-node-config.message-template - - {{ 'rule-node-config.message-template-required' | translate }} - + @if (slackConfigForm.get('messageTemplate').hasError('required')) { + + {{ 'rule-node-config.message-template-required' | translate }} + + } rule-node-config.general-pattern-hint {{ 'rule-node-config.use-system-slack-settings' | translate }} - - rule-node-config.slack-api-token - - - {{ 'rule-node-config.slack-api-token-required' | translate }} - - + @if (!slackConfigForm.get('useSystemSettings').value) { + + rule-node-config.slack-api-token + + @if (slackConfigForm.get('botToken').hasError('required')) { + + {{ 'rule-node-config.slack-api-token-required' | translate }} + + } + + } - - {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }} - + @for (slackChanelType of slackChanelTypes; track slackChanelType) { + + {{ slackChanelTypesTranslateMap.get(slackChanelType) | translate }} + + } rule-node-config.topic-arn-pattern - - {{ 'rule-node-config.topic-arn-pattern-required' | translate }} - + @if (snsConfigForm.get('topicArnPattern').hasError('required')) { + + {{ 'rule-node-config.topic-arn-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint rule-node-config.aws-access-key-id - - {{ 'rule-node-config.aws-access-key-id-required' | translate }} - + @if (snsConfigForm.get('accessKeyId').hasError('required')) { + + {{ 'rule-node-config.aws-access-key-id-required' | translate }} + + } rule-node-config.aws-secret-access-key - - {{ 'rule-node-config.aws-secret-access-key-required' | translate }} - + @if (snsConfigForm.get('secretAccessKey').hasError('required')) { + + {{ 'rule-node-config.aws-secret-access-key-required' | translate }} + + } rule-node-config.aws-region - - {{ 'rule-node-config.aws-region-required' | translate }} - + @if (snsConfigForm.get('region').hasError('required')) { + + {{ 'rule-node-config.aws-region-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html index 8e3de40601..0e07578f19 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html @@ -19,29 +19,39 @@ rule-node-config.queue-type - - {{ sqsQueueTypeTranslationsMap.get(type) | translate }} - + @for (type of sqsQueueTypes; track type) { + + {{ sqsQueueTypeTranslationsMap.get(type) | translate }} + + } rule-node-config.queue-url-pattern - - {{ 'rule-node-config.queue-url-pattern-required' | translate }} - + @if (sqsConfigForm.get('queueUrlPattern').hasError('required')) { + + {{ 'rule-node-config.queue-url-pattern-required' | translate }} + + } rule-node-config.general-pattern-hint - - rule-node-config.delay-seconds - - - {{ 'rule-node-config.min-delay-seconds-message' | translate }} - - - {{ 'rule-node-config.max-delay-seconds-message' | translate }} - - + @if (sqsConfigForm.get('queueType').value === sqsQueueType.STANDARD) { + + rule-node-config.delay-seconds + + @if (sqsConfigForm.get('delaySeconds').hasError('min')) { + + {{ 'rule-node-config.min-delay-seconds-message' | translate }} + + } + @if (sqsConfigForm.get('delaySeconds').hasError('max')) { + + {{ 'rule-node-config.max-delay-seconds-message' | translate }} + + } + + }
rule-node-config.aws-access-key-id - - {{ 'rule-node-config.aws-access-key-id-required' | translate }} - + @if (sqsConfigForm.get('accessKeyId').hasError('required')) { + + {{ 'rule-node-config.aws-access-key-id-required' | translate }} + + } rule-node-config.aws-secret-access-key - - {{ 'rule-node-config.aws-secret-access-key-required' | translate }} - + @if (sqsConfigForm.get('secretAccessKey').hasError('required')) { + + {{ 'rule-node-config.aws-secret-access-key-required' | translate }} + + } rule-node-config.aws-region - - {{ 'rule-node-config.aws-region-required' | translate }} - + @if (sqsConfigForm.get('region').hasError('required')) { + + {{ 'rule-node-config.aws-region-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts index ee6cfac5da..7e5f93de67 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.ts @@ -30,7 +30,7 @@ export class SqsConfigComponent extends RuleNodeConfigurationComponent { sqsConfigForm: UntypedFormGroup; sqsQueueType = SqsQueueType; - sqsQueueTypes = Object.keys(SqsQueueType); + sqsQueueTypes = Object.values(SqsQueueType); sqsQueueTypeTranslationsMap = sqsQueueTypeTranslations; constructor(private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html index 07a1d1068a..3e2773037d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html @@ -21,9 +21,11 @@ {{ 'relation.direction' | translate }} - - {{ entitySearchDirectionTranslationsMap.get(direction) | translate }} rule-node-config.relations-query-config-direction-suffix - + @for (direction of entitySearchDirection; track direction) { + + {{ entitySearchDirectionTranslationsMap.get(direction) | translate }} rule-node-config.relations-query-config-direction-suffix + + }
-
- - - - -
+ @if (checkRelationConfigForm.get('checkForSingleEntity').value) { +
+ + + @if (checkRelationConfigForm.get('entityType').value) { + + + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.html index ec2a5ba89a..aee5db1a19 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/gps-geo-filter-config.component.html @@ -23,19 +23,23 @@ {{ 'rule-node-config.latitude-field-name' | translate }} - - {{ 'rule-node-config.latitude-field-name-required' | translate }} - + @if (geoFilterConfigForm.get('latitudeKeyName').hasError('required')) { + + {{ 'rule-node-config.latitude-field-name-required' | translate }} + + } {{ 'rule-node-config.longitude-field-name' | translate }} - - {{ 'rule-node-config.longitude-field-name-required' | translate }} - + @if (geoFilterConfigForm.get('longitudeKeyName').hasError('required')) { + + {{ 'rule-node-config.longitude-field-name-required' | translate }} + + } -
rule-node-config.coordinate-field-hint
+
rule-node-config.coordinate-field-hint
@@ -44,13 +48,15 @@ {{ 'rule-node-config.perimeter-type' | translate }} - - {{ perimeterTypeTranslationMap.get(type) | translate }} - + @for (type of perimeterTypes; track type) { + + {{ perimeterTypeTranslationMap.get(type) | translate }} + + }
+ class="tb-form-row no-border no-padding slide-toggle"> {{ 'rule-node-config.fetch-perimeter-info-from-metadata' | translate }}
- - {{ 'rule-node-config.perimeter-key-name' | translate }} - - - {{ 'rule-node-config.perimeter-key-name-required' | translate }} - - {{ 'rule-node-config.perimeter-key-name-hint' | translate }} - -
-
- - {{ 'rule-node-config.circle-center-latitude' | translate }} - - - {{ 'rule-node-config.circle-center-latitude-required' | translate }} + @if (geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { + + {{ 'rule-node-config.perimeter-key-name' | translate }} + + @if (geoFilterConfigForm.get('perimeterKeyName').hasError('required')) { + + {{ 'rule-node-config.perimeter-key-name-required' | translate }} - - - {{ 'rule-node-config.circle-center-longitude' | translate }} - - - {{ 'rule-node-config.circle-center-longitude-required' | translate }} - - + } + {{ 'rule-node-config.perimeter-key-name-hint' | translate }} + + } + @if (geoFilterConfigForm.get('perimeterType').value === perimeterType.CIRCLE && + !geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { +
+
+ + {{ 'rule-node-config.circle-center-latitude' | translate }} + + @if (geoFilterConfigForm.get('centerLatitude').hasError('required')) { + + {{ 'rule-node-config.circle-center-latitude-required' | translate }} + + } + + + {{ 'rule-node-config.circle-center-longitude' | translate }} + + @if (geoFilterConfigForm.get('centerLongitude').hasError('required')) { + + {{ 'rule-node-config.circle-center-longitude-required' | translate }} + + } + +
+
+ + {{ 'rule-node-config.range' | translate }} + + @if (geoFilterConfigForm.get('range').hasError('required')) { + + {{ 'rule-node-config.range-required' | translate }} + + } + + + {{ 'rule-node-config.range-units' | translate }} + + @for (type of rangeUnits; track type) { + + {{ rangeUnitTranslationMap.get(type) | translate }} + + } + + @if (geoFilterConfigForm.get('rangeUnit').hasError('required')) { + + {{ 'rule-node-config.range-units-required' | translate }} + + } + +
-
- - {{ 'rule-node-config.range' | translate }} - - - {{ 'rule-node-config.range-required' | translate }} + } + @if (geoFilterConfigForm.get('perimeterType').value === perimeterType.POLYGON && + !geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { + + {{ 'rule-node-config.polygon-definition' | translate }} + + {{ 'rule-node-config.polygon-definition-hint' | translate }} + @if (geoFilterConfigForm.get('polygonsDefinition').hasError('required')) { + + {{ 'rule-node-config.polygon-definition-required' | translate }} - - - {{ 'rule-node-config.range-units' | translate }} - - - {{ rangeUnitTranslationMap.get(type) | translate }} - - - - {{ 'rule-node-config.range-units-required' | translate }} - - -
-
- - {{ 'rule-node-config.polygon-definition' | translate }} - - {{ 'rule-node-config.polygon-definition-hint' | translate }} - - {{ 'rule-node-config.polygon-definition-required' | translate }} - - + } + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/script-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/script-config.component.html index bc0ce9bfcb..4146e478ea 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/script-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/script-config.component.html @@ -16,41 +16,47 @@ -->
- - - - - - - + @if (tbelEnabled) { + + } + @if (scriptConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (scriptConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.html index 28ff15604e..42b8f371a9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/switch-config.component.html @@ -16,41 +16,47 @@ -->
- - - - - - - + @if (tbelEnabled) { + + } + @if (switchConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (switchConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/rule-node-config.models.ts b/ui-ngx/src/app/modules/home/components/rule-node/rule-node-config.models.ts index f326e7e9ce..7a2bec7e33 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/rule-node-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/rule-node-config.models.ts @@ -827,7 +827,7 @@ export enum AttributeScopeResult { SERVER_SCOPE = 'SERVER_SCOPE' } -export const AttributeScopeMap = new Map([ +export const AttributeScopeMap = new Map([ [AttributeScope.SHARED_SCOPE, 'rule-node-config.shared-scope'], [AttributeScope.SERVER_SCOPE, 'rule-node-config.server-scope'], [AttributeScope.CLIENT_SCOPE, 'rule-node-config.client-scope'] diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html index aecafa4164..22d58fb9fc 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html @@ -20,47 +20,56 @@ rule-node-config.new-originator - - {{ originatorSourceTranslationMap.get(changeOriginatorConfigForm.get('originatorSource').value) | translate }} - + @if (originatorSourceTranslationMap.get(changeOriginatorConfigForm.get('originatorSource').value)) { + + {{ originatorSourceTranslationMap.get(changeOriginatorConfigForm.get('originatorSource').value) | translate }} + + } - - - {{ originatorSourceTranslationMap.get(source) | translate }} - -
- - {{ originatorSourceDescTranslationMap.get(source) | translate }} - -
-
- -
- - -
- + + {{ originatorSourceTranslationMap.get(source) | translate }} + +
+ + {{ originatorSourceDescTranslationMap.get(source) | translate }} + + + } + + + @if (changeOriginatorConfigForm.get('originatorSource').value === originatorSource.ENTITY) { +
+ + +
+ + + + rule-node-config.entity-name-pattern + + @if (changeOriginatorConfigForm.get('entityNamePattern').hasError('required') || + changeOriginatorConfigForm.get('entityNamePattern').hasError('pattern')) { + + {{ 'rule-node-config.entity-name-pattern-required' | translate }} + + } + +
+
+ } + @if (changeOriginatorConfigForm.get('originatorSource').value === originatorSource.RELATED) { + -
- - rule-node-config.entity-name-pattern - - - {{ 'rule-node-config.entity-name-pattern-required' | translate }} - - -
-
- - -
+ formControlName="relationsQuery"> + + } +
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html index 001c0aadac..63abc20253 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html @@ -19,41 +19,55 @@ {{'rule-node-config.interval' | translate}} - - {{'rule-node-config.interval-required' | translate}} - - - {{'rule-node-config.interval-min-error' | translate}} - + @if (deduplicationConfigForm.get('interval').hasError('required')) { + + {{'rule-node-config.interval-required' | translate}} + + } + @if (deduplicationConfigForm.get('interval').hasError('min')) { + + {{'rule-node-config.interval-min-error' | translate}} + + } help + matTooltip="{{ 'rule-node-config.interval-hint' | translate }}">help
rule-node-config.strategy
- - {{ deduplicationStrategiesTranslations.get(strategy) | translate }} - + @for (strategy of deduplicationStrategies; track strategy) { + + {{ deduplicationStrategiesTranslations.get(strategy) | translate }} + + } - - - - - - -
- - -
+ @if (deduplicationConfigForm.get('strategy').value === 'ALL') { + + + } + @if (deduplicationConfigForm.get('strategy').value === 'FIRST') { + + + } + @if (deduplicationConfigForm.get('strategy').value === 'LAST') { + + + } + @if (deduplicationConfigForm.get('strategy').value === deduplicationStrategie.ALL) { +
+ + +
+ }
@@ -64,34 +78,46 @@ {{'rule-node-config.max-pending-msgs' | translate}} - - {{'rule-node-config.max-pending-msgs-required' | translate}} - - - {{'rule-node-config.max-pending-msgs-max-error' | translate}} - - - {{'rule-node-config.max-pending-msgs-min-error' | translate}} - + @if (deduplicationConfigForm.get('maxPendingMsgs').hasError('required')) { + + {{'rule-node-config.max-pending-msgs-required' | translate}} + + } + @if (deduplicationConfigForm.get('maxPendingMsgs').hasError('max')) { + + {{'rule-node-config.max-pending-msgs-max-error' | translate}} + + } + @if (deduplicationConfigForm.get('maxPendingMsgs').hasError('min')) { + + {{'rule-node-config.max-pending-msgs-min-error' | translate}} + + } help + matTooltip="{{ 'rule-node-config.max-pending-msgs-hint' | translate }}">help {{'rule-node-config.max-retries' | translate}} - - {{'rule-node-config.max-retries-required' | translate}} - - - {{'rule-node-config.max-retries-max-error' | translate}} - - - {{'rule-node-config.max-retries-min-error' | translate}} - + @if (deduplicationConfigForm.get('maxRetries').hasError('required')) { + + {{'rule-node-config.max-retries-required' | translate}} + + } + @if (deduplicationConfigForm.get('maxRetries').hasError('max')) { + + {{'rule-node-config.max-retries-max-error' | translate}} + + } + @if (deduplicationConfigForm.get('maxRetries').hasError('min')) { + + {{'rule-node-config.max-retries-min-error' | translate}} + + } help + matTooltip="{{ 'rule-node-config.max-retries-hint' | translate }}">help
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts index 77c95515d3..f37b71299f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts @@ -31,7 +31,7 @@ export class DeduplicationConfigComponent extends RuleNodeConfigurationComponent deduplicationConfigForm: FormGroup; deduplicationStrategie = FetchMode; - deduplicationStrategies = Object.keys(this.deduplicationStrategie); + deduplicationStrategies = Object.values(this.deduplicationStrategie); deduplicationStrategiesTranslations = deduplicationStrategiesTranslations; constructor(private fb: FormBuilder) { @@ -77,4 +77,8 @@ export class DeduplicationConfigComponent extends RuleNodeConfigurationComponent protected validatorTriggers(): string[] { return ['strategy']; } + + getDeduplicationStrategiesTranslations(strategy: string) { + return this.deduplicationStrategiesTranslations.get(strategy as FetchMode); + } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/node-json-path-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/node-json-path-config.component.html index f2d4cc6e5f..673a8b74ad 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/node-json-path-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/node-json-path-config.component.html @@ -20,6 +20,8 @@ {{ 'rule-node-config.json-path-expression' | translate }} {{ 'rule-node-config.json-path-expression-hint' | translate }} - {{ 'rule-node-config.json-path-expression-required' | translate }} + @if (jsonPathConfigForm.get('jsonPath').hasError('required')) { + {{ 'rule-node-config.json-path-expression-required' | translate }} + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.html index 89ce705ea9..8aeab13cd9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/rename-keys-config.component.html @@ -20,9 +20,11 @@
- - {{ data.name }} - + @for (data of renameIn; track data) { + + {{ data.name }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.html index 7b39ba131d..8d73d4c0ff 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/script-config.component.html @@ -16,41 +16,47 @@ -->
- - - - - - - + @if (tbelEnabled) { + + } + @if (scriptConfigForm.get('scriptLang').value === scriptLanguage.JS) { + + + + } + @if (scriptConfigForm.get('scriptLang').value === scriptLanguage.TBEL) { + + + + }
- - {{ 'rule-node-config.from-template-required' | translate }} - + @if (toEmailConfigForm.get('fromTemplate').hasError('required')) { + + {{ 'rule-node-config.from-template-required' | translate }} + + } @@ -46,7 +48,7 @@
rule-node-config.recipients
+ popupHelpLink="rulenode/to_email_node_fields_templatization">
@@ -57,10 +59,12 @@ class="tb-enable-vertical-resize" formControlName="toTemplate" cdkTextareaAutosize - cdkAutosizeMinRows="1"> - - {{ 'rule-node-config.to-template-required' | translate }} - + cdkAutosizeMinRows="1"> + @if (toEmailConfigForm.get('toTemplate').hasError('required')) { + + {{ 'rule-node-config.to-template-required' | translate }} + + } rule-node-config.cc-template @@ -68,7 +72,7 @@ formControlName="ccTemplate" class="tb-enable-vertical-resize" cdkTextareaAutosize - cdkAutosizeMinRows="1"> + cdkAutosizeMinRows="1"> rule-node-config.bcc-template @@ -76,14 +80,14 @@ formControlName="bccTemplate" class="tb-enable-vertical-resize" cdkTextareaAutosize - cdkAutosizeMinRows="1"> + cdkAutosizeMinRows="1">
rule-node-config.message-subject-and-content
+ popupHelpLink="rulenode/to_email_node_fields_templatization">
@@ -93,47 +97,57 @@ formControlName="subjectTemplate" class="tb-enable-vertical-resize" cdkTextareaAutosize - cdkAutosizeMinRows="1"> - - {{ 'rule-node-config.subject-template-required' | translate }} - + cdkAutosizeMinRows="1"> + @if (toEmailConfigForm.get('subjectTemplate').hasError('required')) { + + {{ 'rule-node-config.subject-template-required' | translate }} + + } rule-node-config.mail-body-type - - {{ getBodyTypeName() | translate }} - + @if (toEmailConfigForm.get('mailBodyType').value) { + + {{ getBodyTypeName() | translate }} + + } - - - {{ type.name | translate }} - -
- - {{ type.description | translate }} - -
-
-
- - rule-node-config.body-type-template - - rule-node-config.mail-body-types.after-template-evaluation-hint - - - rule-node-config.body-template - - - {{ 'rule-node-config.body-template-required' | translate }} - - -
-
-
+ @for (type of mailBodyTypes; track type) { + + + {{ type.name | translate }} + +
+ + {{ type.description | translate }} + +
+ } + +
+ @if (toEmailConfigForm.get('mailBodyType').value === 'dynamic') { + + rule-node-config.body-type-template + + rule-node-config.mail-body-types.after-template-evaluation-hint + + } + + rule-node-config.body-template + + @if (toEmailConfigForm.get('bodyTemplate').hasError('required')) { + + {{ 'rule-node-config.body-template-required' | translate }} + + } + + + + diff --git a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html index 3beaa93ed1..fea9dafc09 100644 --- a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html @@ -19,23 +19,29 @@ admin.aws-access-key-id - - {{ 'admin.aws-access-key-id-required' | translate }} - + @if (awsSnsProviderConfigurationFormGroup.get('accessKeyId').hasError('required')) { + + {{ 'admin.aws-access-key-id-required' | translate }} + + } admin.aws-secret-access-key - - {{ 'admin.aws-secret-access-key-required' | translate }} - + @if (awsSnsProviderConfigurationFormGroup.get('secretAccessKey').hasError('required')) { + + {{ 'admin.aws-secret-access-key-required' | translate }} + + } admin.aws-region - - {{ 'admin.aws-region-required' | translate }} - + @if (awsSnsProviderConfigurationFormGroup.get('region').hasError('required')) { + + {{ 'admin.aws-region-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html index bc1e9430c7..08545e3a9b 100644 --- a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html @@ -20,41 +20,51 @@ admin.smpp-provider.smpp-version - - {{smppVersion.value}} - + @for (smppVersion of smppVersions; track smppVersion) { + + {{smppVersion.value}} + + } admin.smpp-provider.smpp-host - - {{'admin.smpp-provider.smpp-host-required' | translate}} - + @if (smppSmsProviderConfigurationFormGroup.get('host').hasError('required')) { + + {{'admin.smpp-provider.smpp-host-required' | translate}} + + } admin.smpp-provider.smpp-port - - {{'admin.smpp-provider.smpp-port-required' | translate}} - + @if (smppSmsProviderConfigurationFormGroup.get('port').hasError('required')) { + + {{'admin.smpp-provider.smpp-port-required' | translate}} + + }
admin.smpp-provider.system-id - - {{'admin.smpp-provider.system-id-required' | translate}} - + @if (smppSmsProviderConfigurationFormGroup.get('systemId').hasError('required')) { + + {{'admin.smpp-provider.system-id-required' | translate}} + + } admin.smpp-provider.password - - {{'admin.smpp-provider.password-required' | translate}} - + @if (smppSmsProviderConfigurationFormGroup.get('password').hasError('required')) { + + {{'admin.smpp-provider.password-required' | translate}} + + }
@@ -72,9 +82,11 @@ admin.smpp-provider.bind-type - - {{bindTypesTranslation.get(bindType) | translate}} - + @for (bindType of bindTypes; track bindType) { + + {{bindTypesTranslation.get(bindType) | translate}} + + } @@ -97,17 +109,21 @@ admin.smpp-provider.source-ton - - {{typeOfNumberMap.get(sourceTon).name | translate}} - + @for (sourceTon of typeOfNumber; track sourceTon) { + + {{typeOfNumberMap.get(sourceTon).name | translate}} + + } admin.smpp-provider.source-npi - - {{numberingPlanIdentificationMap.get(sourceNpi).name | translate}} - + @for (sourceNpi of numberingPlanIdentification; track sourceNpi) { + + {{numberingPlanIdentificationMap.get(sourceNpi).name | translate}} + + } @@ -122,17 +138,21 @@ admin.smpp-provider.destination-ton - - {{typeOfNumberMap.get(destinationTon).name | translate}} - + @for (destinationTon of typeOfNumber; track destinationTon) { + + {{typeOfNumberMap.get(destinationTon).name | translate}} + + } admin.smpp-provider.destination-npi - - {{numberingPlanIdentificationMap.get(destinationNpi).name | translate}} - + @for (destinationNpi of numberingPlanIdentification; track destinationNpi) { + + {{numberingPlanIdentificationMap.get(destinationNpi).name | translate}} + + } @@ -151,9 +171,11 @@ admin.smpp-provider.coding-scheme - - {{codingSchemesMap.get(codingScheme).name | translate}} - + @for (codingScheme of codingSchemes; track codingScheme) { + + {{codingSchemesMap.get(codingScheme).name | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts index 35111be91b..4a4341364b 100644 --- a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts @@ -68,16 +68,16 @@ export class SmppSmsProviderConfigurationComponent implements ControlValueAcces smppVersions = smppVersions; - bindTypes = Object.keys(BindTypes); + bindTypes = Object.values(BindTypes); bindTypesTranslation = bindTypesTranslationMap; - typeOfNumber = Object.keys(TypeOfNumber); + typeOfNumber = Object.values(TypeOfNumber); typeOfNumberMap = typeOfNumberMap; - numberingPlanIdentification = Object.keys(NumberingPlanIdentification); + numberingPlanIdentification = Object.values(NumberingPlanIdentification); numberingPlanIdentificationMap = numberingPlanIdentificationMap; - codingSchemes = Object.keys(CodingSchemes); + codingSchemes = Object.values(CodingSchemes); codingSchemesMap = codingSchemesMap; private propagateChange = (v: any) => { }; diff --git a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html index 544f8866f0..620748406e 100644 --- a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html @@ -19,32 +19,38 @@ admin.sms-provider-type - - {{smsProviderTypeTranslations.get(type) | translate}} - + @for (type of smsProviderTypes; track type) { + + {{smsProviderTypeTranslations.get(type) | translate}} + + } - - {{ 'admin.sms-provider-type-required' | translate }} - + @if (smsProviderConfigurationFormGroup.get('type').hasError('required')) { + + {{ 'admin.sms-provider-type-required' | translate }} + + } -
- - - - - - - - - - - - +
+ @switch (smsProviderConfigurationFormGroup.get('type').value) { + @case (smsProviderType.AWS_SNS) { + + + } + @case (smsProviderType.TWILIO) { + + + } + @case (smsProviderType.SMPP) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts index adb477d8fb..86f07cfaaf 100644 --- a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.ts @@ -47,7 +47,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export class SmsProviderConfigurationComponent implements ControlValueAccessor, OnInit { smsProviderType = SmsProviderType; - smsProviderTypes = Object.keys(SmsProviderType); + smsProviderTypes = Object.values(SmsProviderType); smsProviderTypeTranslations = smsProviderTypeTranslationMap; smsProviderConfigurationFormGroup: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html index 99d64302ec..f3c1261ab6 100644 --- a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html @@ -19,27 +19,35 @@ admin.number-from - - {{ 'admin.number-from-required' | translate }} - - - {{ 'admin.phone-number-pattern-twilio' | translate }} - + @if (twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('required')) { + + {{ 'admin.number-from-required' | translate }} + + } + @if (twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('pattern')) { + + {{ 'admin.phone-number-pattern-twilio' | translate }} + + } admin.twilio-account-sid - - {{ 'admin.twilio-account-sid-required' | translate }} - + @if (twilioSmsProviderConfigurationFormGroup.get('accountSid').hasError('required')) { + + {{ 'admin.twilio-account-sid-required' | translate }} + + } admin.twilio-account-token - - {{ 'admin.twilio-account-token-required' | translate }} - + @if (twilioSmsProviderConfigurationFormGroup.get('accountToken').hasError('required')) { + + {{ 'admin.twilio-account-token-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html index 548a7b79c3..00d1f9f782 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html @@ -24,81 +24,92 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
admin.auto-commit-entities
-
- - -
- -
-
-
-
- - -
-
- -
- -
-
- - -
- + + +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+ - + formControlName="entityType" + required + [filterAllowedEntityTypes]="false" + [allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)"> + +
+ + +
+
+
+ @if (entityTypeFormGroup.get('entityType').value === entityTypes.DEVICE) { + + {{ 'version-control.export-credentials' | translate }} + + } + + {{ 'version-control.export-attributes' | translate }} + + + {{ 'version-control.export-relations' | translate }} + + @if (typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)) { + + {{ 'version-control.export-calculated-fields' | translate }} + + }
-
-
- - {{ 'version-control.export-credentials' | translate }} - - - {{ 'version-control.export-attributes' | translate }} - - - {{ 'version-control.export-relations' | translate }} - - - {{ 'version-control.export-calculated-fields' | translate }} -
-
- - -
-
- admin.no-auto-commit-entities-prompt -
+ + +
+ } + @if (!entityTypesFormGroupArray().length) { +
+ admin.no-auto-commit-entities-prompt +
+ }
-
version-control.auto-commit-settings-read-only-hint
+ @if (isReadOnly | async) { +
version-control.auto-commit-settings-read-only-hint
+ }
- - - {{ 'version-control.differences' | translate : {count: diffCount} }} - - - - -
-
{{ 'version-control.current' | translate }}
-
-
{{ versionIdContent() }}
-
- + @if (contentReady) { + +

{{ 'version-control.diff-entity-with-version' | translate: {versionName} }}

+ + + + + {{ 'version-control.differences' | translate : {count: diffCount} }} + + + +
+ } + @if (contentReady) { +
+
{{ 'version-control.current' | translate }}
+
+
{{ versionIdContent() }}
+
+ } + @if (!contentReady) { + + }
-
- -
+ @if (contentReady) { +
+ +
+ } diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html index 829c1414db..32a39a713e 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html @@ -34,13 +34,15 @@
- + @if (singleEntityMode) { + + }
- + @if (!singleEntityMode) { + + }
@@ -129,31 +133,37 @@ + [class.min-w-10]="!singleEntityMode" [class.max-w-10]="!singleEntityMode" [class.w-10]="!singleEntityMode">
- - - + @if (singleEntityMode) { + + } + @if (singleEntityMode) { + + } + @if (!singleEntityMode) { + + }
@@ -162,12 +172,12 @@ {{ - singleEntityMode - ? 'version-control.no-entity-versions-text' - : 'version-control.no-versions-text' - }} + singleEntityMode + ? 'version-control.no-entity-versions-text' + : 'version-control.no-versions-text' + }} {{ 'common.loading' | translate }} + class="no-data-found flex items-center justify-center">{{ 'common.loading' | translate }} + showFirstLastButtons> diff --git a/ui-ngx/src/app/modules/home/components/vc/version-control.component.html b/ui-ngx/src/app/modules/home/components/vc/version-control.component.html index e02052c2b1..4a71456920 100644 --- a/ui-ngx/src/app/modules/home/components/vc/version-control.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/version-control.component.html @@ -15,13 +15,14 @@ limitations under the License. --> - - - +@if (!(hasRepository$ | async)) { + + +} @else { - + (versionRestored)="versionRestored.emit()"> +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html index 7df9a4893f..c97d715c41 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
close - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
@@ -35,57 +39,66 @@
{{'widget-config.action-source' | translate}}*
- - {{ actionSourceName(actionSourceItem.value) }} - + @for (actionSourceItem of data.actionsData.actionSources | keyvalue; track actionSourceItem) { + + {{ actionSourceName(actionSourceItem.value) }} + + } - - warning - - -
-
-
{{'widget-config.column-index' | translate}}*
- - - - {{ getCellClickColumnInfo($index, column) | customTranslate }} - - - - warning - + @if (widgetActionFormGroup.get('actionSourceId').hasError('required') + && widgetActionFormGroup.get('actionSourceId').touched) { + + warning + + }
+ @if (widgetActionFormGroup.get('actionSourceId').value === 'cellClick') { +
+
{{'widget-config.column-index' | translate}}*
+ + + @for (column of configuredColumns; track column) { + + {{ getCellClickColumnInfo($index, column) | customTranslate }} + + } + + @if (widgetActionFormGroup.get('columnIndex').hasError('required') && + widgetActionFormGroup.get('columnIndex').touched) { + + warning + + } + +
+ }
{{'widget-config.action-name' | translate}}*
- - warning - + class="tb-error"> + warning + + }
@if (widgetActionFormGroup.get('actionSourceId').value === 'headerButton') { @@ -95,9 +108,11 @@
widget-config.header-button.button-type
- - {{ widgetHeaderActionButtonTypeTranslationMap.get(widgetHeaderActionButtonType[button]) | translate }} - + @for (button of widgetHeaderActionButtonTypes; track button) { + + {{ widgetHeaderActionButtonTypeTranslationMap.get(widgetHeaderActionButtonType[button]) | translate }} + + }
@@ -156,35 +171,37 @@
} -
- - - - - {{ 'widget-config.show-hide-action-using-function' | translate }} - - - - - - - -
+ @if (displayShowWidgetActionForm()) { +
+ + + + + {{ 'widget-config.show-hide-action-using-function' | translate }} + + + + + + + +
+ } + formControlName="widgetAction" + [callbacks]="data.callbacks" + [widgetType]="data.widgetType" + [additionalWidgetActionTypes]="data.additionalWidgetActionTypes"> diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html index 5bab97a12d..454082ff54 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html @@ -16,14 +16,15 @@ --> - - + @if (!widgetEditMode) { + + + }
widgets.action-button.behavior
@@ -44,7 +45,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="activatedState"> + formControlName="activatedState">
widgets.button-state.disabled-state
@@ -55,7 +56,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html index 51ebab7a92..71edff2c83 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html @@ -30,7 +30,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.power-button.power-on
@@ -39,7 +39,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="onUpdateState"> + formControlName="onUpdateState">
widgets.power-button.power-off
@@ -48,7 +48,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="offUpdateState"> + formControlName="offUpdateState">
widgets.rpc-state.disabled-state
@@ -59,7 +59,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -70,12 +70,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout"> - - {{ powerButtonLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout"> + @for (layout of powerButtonLayouts; track layout) { + + {{ powerButtonLayoutTranslationMap.get(layout) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html index f748c13f81..fd4790749c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html @@ -16,14 +16,15 @@ --> - - + @if (!widgetEditMode) { + + + }
widgets.action-button.behavior
@@ -37,7 +38,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
@@ -68,7 +69,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -80,12 +81,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout"> - - {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout"> + @for (layout of segmentedButtonLayouts; track layout) { + + {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} + + }
@@ -224,16 +227,18 @@ -
-
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
- - -
+ @for (state of widgetButtonToggleStates; track state) { +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+ }
@@ -265,16 +270,18 @@ -
-
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
- - -
+ @for (state of widgetButtonToggleStates; track state) { +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html index 8b29fe82aa..25e9bd78da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html @@ -28,17 +28,23 @@
widgets.aggregated-value-card.arrow
-
-
- - + @if (keysFormArray().controls.length) { +
+ @for (keyControl of keysFormArray().controls; track trackByKey($index, keyControl)) { +
+ + +
+ }
-
+ } @else { + {{ 'widgets.aggregated-value-card.no-values' | translate }} + }
- - {{ 'widgets.aggregated-value-card.no-values' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html index 90cb150339..e4daba1f6d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - -
-
- - {{ 'admin.mobile-app.use-system-settings' | translate }} - -
-
-
-
-
widget-config.appearance
-
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
-
- - {{ 'admin.mobile-app.badges' | translate }} - - - - - {{ badgePosition.value | translate }} - - - -
-
- - {{ 'admin.mobile-app.label' | translate }} +@if (mobileAppQrCodeWidgetConfigForm) { + +
+
+ + {{ 'admin.mobile-app.use-system-settings' | translate }} - - -
-
- - {{ 'widgets.liquid-level-card.title' | translate }} - -
- - - - - - - +
+ @if (!mobileAppQrCodeWidgetConfigForm.get('useSystemSettings').value) { +
+
+
widget-config.appearance
+
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + @for (badgePosition of badgePositionTranslationsMap | keyvalue; track badgePosition) { + + {{ badgePosition.value | translate }} + + } + + +
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + +
+
+ + {{ 'widgets.liquid-level-card.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.liquid-level-card.icon' | translate }} + +
+ + + + + + + + +
+
-
-
- - {{ 'widgets.liquid-level-card.icon' | translate }} - -
- - - - - - - - +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+ +
-
-
-
widget-config.card-appearance
-
-
{{ 'widgets.background.background' | translate }}
- - -
-
-
widget-config.show-card-buttons
- - {{ 'fullscreen.fullscreen' | translate }} - -
-
-
{{ 'widget-config.card-border-radius' | translate }}
- - - -
-
-
{{ 'widget-config.card-padding' | translate }}
- - - -
-
- - -
- + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html index 120ccf6457..756b7443c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - {{ progressBarLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout"> + @for (layout of progressBarLayouts; track layout) { + + {{ progressBarLayoutTranslationMap.get(layout) | translate }} + + }
@@ -126,20 +130,22 @@
-
- - {{ 'widgets.progress-bar.range-ticks' | translate }} - -
- - - - + @if (progressBarWidgetConfigForm.get('layout').value === progressBarLayout.default) { +
+ + {{ 'widgets.progress-bar.range-ticks' | translate }} + +
+ + + + +
-
+ }
widgets.progress-bar.bar
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html index 1e28c9eb85..9e0f15d1f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } widget-config.appearance
widgets.simple-card.label
- + >
widgets.simple-card.label-position
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html index 9a4a6196c8..90bbca8421 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - {{ valueCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout"> + @for (layout of valueCardLayouts; track layout) { + + {{ valueCardLayoutTranslationMap.get(layout) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html index c7943ec0a7..2ec1b80253 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html @@ -34,12 +34,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout"> - - {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout"> + @for (layout of valueChartCardLayouts; track layout) { + + {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} + + }
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 1aa4c2be3d..f927340691 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 @@ -99,7 +99,7 @@
+ formControlName="grid">
@@ -167,41 +167,41 @@
+ formControlName="barBackgroundSettings" + titleText="widgets.chart.background" + fillNoneTitle="widgets.chart.fill-type-solid"> + stroked + formControlName="noAggregationBarWidthSettings">
widgets.time-series-chart.axis.y-axis
+ formControlName="yAxis" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + axisType="yAxis" + hideUnits + hideDecimals>
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + axisType="xAxis">
+ formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + hideYAxis>
@@ -217,9 +217,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -280,7 +282,7 @@
+ formControlName="tooltipDateFormat"> @@ -316,7 +318,7 @@
+ formControlName="animation">
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html index 4515802dfa..7bac268c2b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html @@ -22,16 +22,19 @@
datakey.label
datakey.color
-
-
- - + @if (keysFormArray().controls.length) { +
+ @for (keyControl of keysFormArray().controls; track trackByKey($index, keyControl)) { +
+ + +
+ }
-
+ } @else { + {{ 'widgets.chart.no-series' | translate }} + }
- - {{ 'widgets.chart.no-series' | translate }} - 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 b0f108ba58..f03c43286b 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 @@ -16,32 +16,34 @@ --> - - + @if (displayTimewindowConfig) { + + + } + [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
@@ -121,9 +123,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -174,9 +178,11 @@
- - {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} - + @for (type of latestChartTooltipValueTypes; track type) { + + {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} + + } @@ -210,7 +216,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -239,7 +245,7 @@
+ formControlName="actions"> @@ -254,9 +260,11 @@
- - {{ pieChartLabelPositionTranslationMap.get(position) | translate }} - + @for (position of pieChartLabelPositions; track position) { + + {{ pieChartLabelPositionTranslationMap.get(position) | translate }} + + } - - {{ doughnutLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.doughnut.layout' | translate }}" formControlName="layout"> + @for (layout of doughnutLayouts; track layout) { + + {{ doughnutLayoutTranslationMap.get(layout) | translate }} + + }
@@ -346,8 +356,8 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
@@ -385,9 +395,9 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
@@ -434,9 +444,11 @@
{{ 'widgets.radar-chart.shape' | translate }}
- - {{ radarChartShapeTranslations.get(shape) | translate }} - + @for (shape of radarChartShapes; track shape) { + + {{ radarChartShapeTranslations.get(shape) | translate }} + + }
@@ -454,9 +466,11 @@
- - {{ chartLineTypeTranslations.get(type) | translate }} - + @for (type of chartLineTypes; track type) { + + {{ chartLineTypeTranslations.get(type) | translate }} + + } @@ -472,9 +486,11 @@
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + } @@ -490,9 +506,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + }
+ formControlName="fillAreaSettings">
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 7d3195e7f4..9f597fb501 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 @@ -83,7 +83,7 @@
+ formControlName="grid">
@@ -118,11 +118,11 @@ {{ 'widgets.range-chart.show-range-thresholds' | translate }} + boxButton + hideYAxis + [widgetConfig]="widgetConfig?.config" + title="widgets.range-chart.range-thresholds-settings" + formControlName="rangeThreshold">
@@ -150,9 +150,11 @@ - - {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} - + @for (stepType of lineSeriesStepTypes; track stepType) { + + {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} + + }
@@ -165,9 +167,11 @@
widgets.time-series-chart.line-type
- - {{ chartLineTypeTranslations.get(lineType) | translate }} - + @for (lineType of chartLineTypes; track lineType) { + + {{ chartLineTypeTranslations.get(lineType) | translate }} + + }
@@ -194,9 +198,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + } widgets.time-series-chart.series.point.point-shape
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + }
@@ -241,29 +249,29 @@
widgets.time-series-chart.axis.y-axis
+ formControlName="yAxis" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + axisType="yAxis" + hideUnits + hideDecimals>
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + axisType="xAxis">
+ formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + hideYAxis>
@@ -279,9 +287,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -336,76 +346,76 @@
- - {{ 'tooltip.date' | translate }} - -
- - - - - -
-
-
- -
- {{ 'tooltip.show-date-time-interval' | translate }} -
-
-
-
-
{{ 'tooltip.background-color' | translate }}
+ + {{ 'tooltip.date' | translate }} + +
+ + + + formControlName="tooltipDateColor">
-
-
{{ 'tooltip.background-blur' | translate }}
- - -
px
-
-
- - +
+
+ +
+ {{ 'tooltip.show-date-time-interval' | translate }} +
+
+
+
+
{{ 'tooltip.background-color' | translate }}
+ + +
+
+
{{ 'tooltip.background-blur' | translate }}
+ + +
px
+
+
+ + +
+ + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ +
- - -
-
widget-config.card-appearance
-
-
{{ 'widgets.background.background' | translate }}
- - -
-
-
widget-config.show-card-buttons
- - {{ 'fullscreen.fullscreen' | translate }} - -
-
-
{{ 'widget-config.card-border-radius' | translate }}
- - - -
-
-
{{ 'widget-config.card-padding' | translate }}
- - - -
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + +
- - +
+ + 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 85ef776718..0602e55fc9 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 @@ -19,11 +19,11 @@ + [configMode]="basicMode" + hideDatasourceLabel + hideDataKeys + forceSingleDatasource + formControlName="datasources">
@@ -36,8 +36,8 @@ {{ 'widgets.time-series-chart.comparison.comparison' | translate }}
- - -
- - {{ 'widgets.time-series-chart.comparison.comparison' | translate }} - -
- - - - {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - - {{ 'widgets.chart.time-for-comparison-days' | translate }} - - - {{ 'widgets.chart.time-for-comparison-weeks' | translate }} - - - {{ 'widgets.chart.time-for-comparison-months' | translate }} - - - {{ 'widgets.chart.time-for-comparison-years' | translate }} - - - {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} - - - - - - - + } + @if (seriesMode === 'comparison') { +
+ + {{ 'widgets.time-series-chart.comparison.comparison' | translate }} + +
+ + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + + + + + - + +
-
- - + + }
- - + + }
+ formControlName="grid">
@@ -193,10 +198,11 @@ axisType="xAxis">
- - + @if (chartType === TimeSeriesChartType.default || chartType === TimeSeriesChartType.bar) { + + + }
@@ -268,9 +274,9 @@
{{ 'tooltip.trigger' | translate }}
{{ 'tooltip.trigger-point' | translate }} + [value]="EChartsTooltipTrigger.point">{{ 'tooltip.trigger-point' | translate }} {{ 'tooltip.trigger-axis' | translate }} + [value]="EChartsTooltipTrigger.axis">{{ 'tooltip.trigger-axis' | translate }}
@@ -305,7 +311,7 @@
+ formControlName="tooltipDateFormat"> @@ -325,7 +331,7 @@
- {{ 'tooltip.hide-zero-tooltip-values' | translate }} + {{ 'tooltip.hide-zero-tooltip-values' | translate }}
@@ -380,6 +386,6 @@
+ formControlName="actions"> diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html index d23896289e..b8ea5924a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -16,12 +16,14 @@ -->
- - - {{ 'datakey.timeseries' | translate }} - {{ 'datakey.latest' | translate }} - - + @if (hasAdditionalLatestDataKeys) { + + + {{ 'datakey.timeseries' | translate }} + {{ 'datakey.latest' | translate }} + + + } - - - -
- - - - - {{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }} - - - - {{ timeSeriesChartSeriesTypeIcons.get(type) }} - {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} - - - -
-
- - - - {{ yAxis }} - - - -
-
- - -
-
- - -
-
- - -
widget-config.decimals-suffix
+ @if (!hideDataKeyLabel) { + + -
-
-
-
+ } + @if (timeSeriesChart) { +
+ @if (displayUnitsOrDigits) { + + + @for (yAxis of yAxisIds; track yAxis) { + + {{ yAxis }} + + } + + + } +
+ } + @if (!hideDataKeyColor) { +
+ + +
+ } + @if (!hideUnits && !hideDataKeyUnits && (!singleRow || displayUnitsOrDigits)) { +
+ @if (displayUnitsOrDigits) { + + + } +
+ } + @if (!hideDecimals && !hideDataKeyDecimals && (!singleRow || displayUnitsOrDigits)) { +
+ @if (displayUnitsOrDigits) { + + + @if (singleRow) { +
widget-config.decimals-suffix
+ } +
+ } +
+ } + @if (!singleRow) { +
+
+ @if (modelValue) { + + } +
+
- -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html index d2d8df7139..393ab69ad8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html @@ -18,61 +18,83 @@
-
{{ panelTitle }}
+ @if (!hidePanel) { +
{{ panelTitle }}
+ }
-
datakey.source
+ @if (hasAdditionalLatestDataKeys) { +
datakey.source
+ }
datakey.key
datakey.label
-
widgets.time-series-chart.series.type
-
widgets.time-series-chart.axis.y-axis
-
datakey.color
-
widget-config.units-short
-
widget-config.decimals-short
+ @if (timeSeriesChart && showTimeSeriesType) { +
widgets.time-series-chart.series.type
+ } + @if (timeSeriesChart) { +
widgets.time-series-chart.axis.y-axis
+ } + @if (!hideDataKeyColor) { +
datakey.color
+ } + @if (!hideUnits) { +
widget-config.units-short
+ } + @if (!hideDecimals) { +
widget-config.decimals-short
+ }
-
-
- - -
- -
+ @if (keysFormArray().controls.length) { +
+ @for (keyControl of keysFormArray().controls; track trackByKey($index, keyControl)) { +
+ + +
+ +
+
+ }
-
- + } @else { + {{ noKeysText }} + } + @if (errorText) { + + }
- - {{ noKeysText }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html index 72f4b0f1d8..b493696d4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html @@ -19,18 +19,24 @@
widget-config.actions
- - - {{ widgetAction.icon }} - {{ widgetAction.name }} - - + @for (actionSourceId of widgetActionSourceIds; track actionSourceId) { + @for (widgetAction of widgetActionsByActionSourceId(actionSourceId); track widgetAction) { + + {{ widgetAction.icon }} + {{ widgetAction.name }} + + } + } - - + @if (!hasWidgetActions) { + + } + @if (hasWidgetActions) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.html index 27ad183dfa..8acae32fa7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/entity/entities-table-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } + > - - + @if (displayTimewindowConfig) { + + + }
-
-
widgets.gauge.progress-bar
-
-
widgets.gauge.progress-bar-color-start
- - -
-
-
widgets.gauge.progress-bar-color-end
- - -
-
-
widgets.gauge.bar-stroke-color
- - + @if (gaugeType === 'linear') { +
+
widgets.gauge.progress-bar
+
+
widgets.gauge.progress-bar-color-start
+ + +
+
+
widgets.gauge.progress-bar-color-end
+ + +
+
+
widgets.gauge.bar-stroke-color
+ + +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.html index a5dbfac219..c2470210fb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/compass-gauge-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - + @if (displayTimewindowConfig) { + + + } - - {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.gauge.simple-gauge-type' | translate }}" + formControlName="gaugeType"> + @for (layout of digitalGaugeLayouts; track layout) { + + {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} + + } -
-
{{ 'widgets.gauge.donut-start-angle' | translate }}
- - - -
+ @if (simpleGaugeWidgetConfigForm.get('gaugeType').value === digitalGaugeType.donut) { +
+
{{ 'widgets.gauge.donut-start-angle' | translate }}
+ + + +
+ }
@@ -76,14 +82,15 @@
widgets.gauge.max-value-short
- - warning - + @if (simpleGaugeWidgetConfigForm.get('maxValue').hasError('maxValue')) { + + warning + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html index e36e315b3e..8c8655a0f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - {{ batteryLevelLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.battery-level.layout' | translate }}" formControlName="layout"> + @for (layout of batteryLevelLayouts; track layout) { + + {{ batteryLevelLayoutTranslationMap.get(layout) | translate }} + + } -
-
{{ 'widgets.battery-level.sections-count' | translate }}
- - - -
+ @if (sectionsCountEnabled) { +
+
{{ 'widgets.battery-level.sections-count' | translate }}
+ + + +
+ }
{{ 'widget-config.title' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html index 473bdb0b4b..7ecb3a0441 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html @@ -15,339 +15,364 @@ limitations under the License. --> - - - - - -
-
widget-config.appearance
-
- - {{ 'widgets.liquid-level-card.title' | translate }} - -
- - - - - -
-
-
- - {{ 'widgets.liquid-level-card.icon' | translate }} - -
- - - - - - - - +@if (levelCardWidgetConfigForm) { + + @if (displayTimewindowConfig) { + + + } + + +
+
widget-config.appearance
+
+ + {{ 'widgets.liquid-level-card.title' | translate }} + +
+ + + + + +
-
-
-
-
widgets.liquid-level-card.shape
- - - {{ DataSourceTypeTranslations.get(type) | translate }} - - +
+ + {{ 'widgets.liquid-level-card.icon' | translate }} + +
+ + + + + + + + +
- +
+
widgets.liquid-level-card.shape
+ + @for (type of DataSourceTypes; track type) { + + {{ DataSourceTypeTranslations.get(type) | translate }} + + } + +
+ - - {{ ShapesTranslationMap.get(shape) | translate }} - - - -
-
widgets.liquid-level-card.shape-attribute-name
- - -
-
-
widgets.liquid-level-card.shape-by-attribute
-
+ style="width: 100%;" + label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape"> + @for (shape of shapes; track shape) { + + {{ ShapesTranslationMap.get(shape) | translate }} + + } + + @if (levelCardWidgetConfigForm.get('tankSelectionType').value === DataSourceType.attribute) { +
+
widgets.liquid-level-card.shape-attribute-name
+ +
-
- -
- - - {{ LevelCardLayoutTranslationMap.get(layout) | translate }} - - -
-
-
widgets.liquid-level-card.units
-
-
widgets.liquid-level-card.datasource-units
-
- - +
+
widgets.liquid-level-card.shape-by-attribute
+
+
+
+ }
+ + @for (layout of LevelCardLayouts; track layout) { + + {{ LevelCardLayoutTranslationMap.get(layout) | translate }} + + } +
-
-
widgets.liquid-level-card.widget-units
-
- - - - {{ DataSourceTypeTranslations.get(type) | translate }} - - - - - - - - - +
+
widgets.liquid-level-card.units
+
+
widgets.liquid-level-card.datasource-units
+
+ + +
-
-
-
widgets.liquid-level-card.total-volume
-
- - - - {{ DataSourceTypeTranslations.get(type) | translate }} - - - - - - - warning - - - - - - + @if (levelCardWidgetConfigForm.get('layout').value === LevelCardLayout.absolute) { +
+
widgets.liquid-level-card.widget-units
+
+ + + @for (type of DataSourceTypes; track type) { + + {{ DataSourceTypeTranslations.get(type) | translate }} + + } + + + @if (levelCardWidgetConfigForm.get('widgetUnitsSource').value === DataSourceType.static) { + + + } @else { + + + } +
+
+ } +
+
widgets.liquid-level-card.total-volume
+
+ + + @for (type of DataSourceTypes; track type) { + + {{ DataSourceTypeTranslations.get(type) | translate }} + + } + + + @if (levelCardWidgetConfigForm.get('volumeSource').value === DataSourceType.static) { + + + @if (levelCardWidgetConfigForm.get('volumeConstant').hasError('required')) { + + warning + + } + + } @else { + + + } +
-
-
-
widgets.liquid-level-card.total-volume-units
-
- - - - {{ DataSourceTypeTranslations.get(type) | translate }} - - - - - - - - - +
+
widgets.liquid-level-card.total-volume-units
+
+ + + @for (type of DataSourceTypes; track type) { + + {{ DataSourceTypeTranslations.get(type) | translate }} + + } + + + @if (levelCardWidgetConfigForm.get('volumeUnitsSource').value === DataSourceType.static) { + + + } @else { + + + } +
-
-
-
widgets.liquid-level-card.color-and-font
-
-
widgets.liquid-level-card.tank-color
- - -
-
-
widgets.liquid-level-card.liquid-color
- - -
-
-
widgets.liquid-level-card.value
-
- - -
widget-config.decimals-suffix
-
- - - +
+
widgets.liquid-level-card.color-and-font
+
+
widgets.liquid-level-card.tank-color
+
-
-
-
widgets.liquid-level-card.total-volume
-
- - - - +
+
widgets.liquid-level-card.liquid-color
+ +
-
-
-
- - - - - {{ 'widget-config.tooltip' | translate }} - - - - -
- - {{ 'widgets.liquid-level-card.level' | translate }} - + @if (levelCardWidgetConfigForm.get('layout').value !== LevelCardLayout.simple) { +
+
widgets.liquid-level-card.value
- - - - + +
widget-config.decimals-suffix
- + - +
-
- - {{ 'widgets.value-card.date' | translate }} - -
- - + } + @if (levelCardWidgetConfigForm.get('layout').value === LevelCardLayout.absolute) { +
+
widgets.liquid-level-card.total-volume
+
+ - +
-
-
widgets.liquid-level-card.tooltip-background
- - -
-
-
widgets.liquid-level-card.background-blur
- - -
px
-
-
- - -
-
-
widget-config.card-appearance
-
-
{{ 'widgets.background.background' | translate }}
- - + }
-
-
widget-config.show-card-buttons
- - {{ 'fullscreen.fullscreen' | translate }} - -
-
-
{{ 'widget-config.card-border-radius' | translate }}
- - - +
+ + + + + {{ 'widget-config.tooltip' | translate }} + + + + +
+ + {{ 'widgets.liquid-level-card.level' | translate }} + +
+ + + + +
widget-config.decimals-suffix
+
+ + + + +
+
+
+ + {{ 'widgets.value-card.date' | translate }} + +
+ + + + + +
+
+
+
widgets.liquid-level-card.tooltip-background
+ + +
+
+
widgets.liquid-level-card.background-blur
+ + +
px
+
+
+
+
-
-
{{ 'widget-config.card-padding' | translate }}
- - - +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
-
- - - + + + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html index ed034aaf89..c34a73d6ac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - {{ signalStrengthLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.signal-strength.layout' | translate }}" formControlName="layout"> + @for (layout of signalStrengthLayouts; track layout) { + + {{ signalStrengthLayoutTranslationMap.get(layout) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html index 2d596fd468..450f8227fa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html @@ -30,7 +30,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.rpc-state.disabled-state
@@ -41,7 +41,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -52,12 +52,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout"> - - {{ statusWidgetLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.status-widget.layout' | translate }}" formControlName="layout"> + @for (layout of statusWidgetLayouts; track layout) { + + {{ statusWidgetLayoutTranslationMap.get(layout) | translate }} + + }
@@ -69,16 +71,18 @@ {{ 'widgets.status-widget.off' | translate }}
- - - + } + @if (cardStyleMode === 'off') { + - + + }
widget-config.card-appearance
@@ -102,6 +106,6 @@
+ formControlName="actions"> diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html index 3d1f05fb4b..bfbdc25d9e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } + formControlName="initialState">
widgets.rpc-state.turn-on
@@ -39,7 +39,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="onUpdateState"> + formControlName="onUpdateState">
widgets.rpc-state.turn-off
@@ -48,7 +48,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="offUpdateState"> + formControlName="offUpdateState">
widgets.rpc-state.disabled-state
@@ -59,7 +59,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -70,12 +70,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> - - {{ singleSwitchLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> + @for (layout of singleSwitchLayouts; track layout) { + + {{ singleSwitchLayoutTranslationMap.get(layout) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html index b6566c181e..d9bd4b82f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html @@ -27,7 +27,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.slider.on-value-change
@@ -37,7 +37,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="valueChange"> + formControlName="valueChange">
widgets.rpc-state.disabled-state
@@ -48,7 +48,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -59,12 +59,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout"> - - {{ sliderLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout"> + @for (layout of sliderLayouts; track layout) { + + {{ sliderLayoutTranslationMap.get(layout) | translate }} + + }
@@ -117,25 +119,27 @@
-
- - {{ 'widgets.slider.value' | translate }} - -
- - - -
widget-config.decimals-suffix
-
- - - - + @if (sliderWidgetConfigForm.get('layout').value !== sliderLayout.simplified) { +
+ + {{ 'widgets.slider.value' | translate }} + +
+ + + +
widget-config.decimals-suffix
+
+ + + + +
-
+ }
{{ 'widgets.slider.range' | translate }}
@@ -213,44 +217,48 @@
-
-
- {{ 'widgets.slider.left-icon' | translate }} -
-
- - - - - - - - -
-
-
-
- {{ 'widgets.slider.right-icon' | translate }} + @if (sliderWidgetConfigForm.get('layout').value === sliderLayout.extended) { +
+
+ {{ 'widgets.slider.left-icon' | translate }} +
+
+ + + + + + + + +
-
- - - - - - - - + } + @if (sliderWidgetConfigForm.get('layout').value === sliderLayout.extended) { +
+
+ {{ 'widgets.slider.right-icon' | translate }} +
+
+ + + + + + + + +
-
+ }
widget-config.card-appearance
@@ -279,6 +287,6 @@
+ formControlName="actions"> diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html index d9c654f17b..d2d1e65491 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html @@ -27,7 +27,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.value-stepper.left-button-click
@@ -37,7 +37,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="leftButtonClick"> + formControlName="leftButtonClick">
widgets.value-stepper.right-button-click
@@ -47,7 +47,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="rightButtonClick"> + formControlName="rightButtonClick">
widgets.button-state.disabled-state
@@ -58,7 +58,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -69,12 +69,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> - - {{ valueStepperTypeTranslationMap.get(type) | translate }} - + label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> + @for (type of valueStepperTypes; track type) { + + {{ valueStepperTypeTranslationMap.get(type) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html index 3727cd0952..af1cb2d514 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html @@ -16,10 +16,12 @@ --> - - + @if (displayTimewindowConfig) { + + + } - - - - + @if (!windSpeedDirectionWidgetConfigForm.get('centerValueKey').value) { + + + } + @if (!windSpeedDirectionWidgetConfigForm.get('centerValueKey').value) { + + + }
@@ -64,15 +70,19 @@ hideDataKeyLabel hideDataKeyColor> - - - - + @if (windSpeedDirectionWidgetConfigForm.get('centerValueKey').value) { + + + } + @if (windSpeedDirectionWidgetConfigForm.get('centerValueKey').value) { + + + }
@@ -83,12 +93,14 @@ breakpoints: { 'lt-md': 2 }}" - label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout"> - - {{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout"> + @for (layout of windSpeedDirectionLayouts; track layout) { + + {{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html index 1dacf0cd22..0a276dcbfd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -16,106 +16,126 @@ -->
- - widget-config.datasource-type - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - datasource.label - - - - - - - - - - - - - + @if (!basicMode) { + + widget-config.datasource-type + + @for (datasourceType of datasourceTypes; track datasourceType) { + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + } + + + } +
+ @switch (datasourceFormGroup.get('type').value) { + @case (datasourceType.function) { + @if (!hideDatasourceLabel) { + + datasource.label + + + } + } + @case (datasourceFormGroup.get('type').value === datasourceType.device || + datasourceFormGroup.get('type').value === datasourceType.entity || + datasourceFormGroup.get('type').value === datasourceType.entityCount || + datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : '') { + @if (datasourceFormGroup.get('type').value === datasourceType.alarmCount && !hideAlarmFilter) { + + } + @if (datasourceFormGroup.get('type').value === datasourceType.device) { + + + } + @if (datasourceFormGroup.get('type').value !== datasourceType.device) { + + + } + @if (!hideDatasourceLabel && [datasourceType.entityCount, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)) { + + + + } + } + }
-
- - - - -
- - + @if (!hideDataKeys) { +
+ + + @if (hasAdditionalLatestDataKeys) { + + + } +
+ } + @if ((!basicMode || displayDatasourceFilterForBasicMode) && ![datasourceType.function].includes(datasourceFormGroup.get('type').value)) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html index f1f80b9248..f77c7c3ab4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html @@ -19,23 +19,30 @@
{{ (singleDatasource ? (isAlarmSource ? 'widget-config.alarm-source' : 'widget-config.datasource') : 'widget-config.datasources') | translate }}
-
- {{ 'widget-config.timeseries-key-error' | translate }} -
- - {{ 'device.device' | translate }} - {{ 'entity.entity-alias' | translate }} - + @if (timeseriesKeyError) { +
+ {{ 'widget-config.timeseries-key-error' | translate }} +
+ } + @if (basicMode && !hideDatasourcesMode) { + + {{ 'device.device' | translate }} + {{ 'entity.entity-alias' | translate }} + + }
-
{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}
+ @if (maxDatasources > 1) { +
{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}
+ }
-
- datasource.add-datasource-prompt -
- + @if (datasourcesFormArray.length === 0) { +
+ datasource.add-datasource-prompt +
+ } @else {
- -
-
-
{{$index + 1}}
-
-
-
- - -
- - + @if (!dragDisabled) { + + drag_indicator + + } +
+ }
+
-
-
- + + }
- -
- -
+ } + @if (showAddDatasource) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html index d44c668be8..7c8a8b4a8b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html @@ -23,16 +23,19 @@ {{ 'entity.entity-alias' | translate }}
- - - - + @if (targetDeviceFormGroup.get('type').value === targetDeviceType.device) { + + + } + @if (targetDeviceFormGroup.get('type').value === targetDeviceType.entity) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html index 75179512c9..1b523be28d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html @@ -23,12 +23,16 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
+ [currentState]="state" [dashboard]="dashboard" [parentDashboard]="parentDashboard" style="width: 100%; height: 100%;">
- + @if (allowAcknowledgment) { + + } + @if (ctx.settings.allowClear) { + + }
+ matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> - - - {{ column.title }} - - - - - - + + {{ column.title }} + + + + @if (column.entityKey.key === 'assignee') { + + @if (alarm.assigneeId && checkAssigneeHasName(alarm.assignee)) { + - - {{ getUserInitials(alarm.assignee) }} - - - {{ getUserDisplayName(alarm.assignee) }} - - - - - {{ alarm.assigneeId ? 'no_accounts' : 'account_circle' }} - - {{ (alarm.assigneeId ? 'alarm.user-deleted' : 'alarm.unassigned') | translate }} + + {{ getUserInitials(alarm.assignee) }} + + + {{ getUserDisplayName(alarm.assignee) }} + - - - - - - - + } @else { + + {{ alarm.assigneeId ? 'no_accounts' : 'account_circle' }} + + {{ (alarm.assigneeId ? 'alarm.user-deleted' : 'alarm.unassigned') | translate }} + + + } + @if (allowAssign) { + + } + + } + + + } - + @if (alarmsDatasource.countCellButtonAction) {
-
+ }
- + @if (alarmsDatasource.countCellButtonAction) {
- - - - -
-
- - - - - - + } + }
-
+ @if (alarm.hasActions) { +
+ + + @for (actionDescriptor of alarm.actionCellButtons; track trackByActionCellDescriptionId($index, actionDescriptor)) { + @if (actionDescriptor.icon) { + + } + } + +
+ } + }
@@ -172,20 +188,24 @@ [class.tb-pointer]="hasRowAction" *matRowDef="let alarm; columns: displayedColumns; let row = index" [style]="rowStyle(alarm, row) | async" - (click)="onRowClick($event, alarm)"> + (click)="onRowClick($event, alarm)">
{{ noDataDisplayMessageText }} + class="no-data-found flex items-center justify-center">{{ noDataDisplayMessageText }} {{ 'common.loading' | translate }} + class="no-data-found flex items-center justify-center" >{{ 'common.loading' | translate }}
- - + @if (displayPagination) { + + } + @if (displayPagination) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html index 23e6d9d7dd..067b2b182f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html @@ -26,5 +26,7 @@ [ctx]="ctx" (clicked)="onClick($event)"> - + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.html index 37a9bc5ed3..92a0d87f45 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/toggle-button-widget.component.html @@ -23,12 +23,14 @@ [class.horizontal-fill]="horizontalFill" [class.vertical-fill]="verticalFill"> + [appearance]="appearance" + [autoScale]="autoScale" + [disabled]="disabled || (loading$ | async)" + [ctx]="ctx" + (clicked)="onClick($event)">
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html index f1aee8b5cc..e95506704d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html @@ -29,6 +29,8 @@ (clicked)="onClick($event)"> - + @if (loading$ | async) { + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html index cca727e061..1490b9074b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html @@ -21,69 +21,83 @@ - + @if (showValues) { - + } -
{{ subtitle$ | async }}
+ @if (showSubtitle) { +
{{ subtitle$ | async }}
+ }
- + @if (values[aggregatedValueCardKeyPosition.leftTop]) { - - + } + @if (values[aggregatedValueCardKeyPosition.leftBottom]) { - + }
- + @if (values[aggregatedValueCardKeyPosition.center]) { - + }
- + @if (values[aggregatedValueCardKeyPosition.rightTop]) { - - + } + @if (values[aggregatedValueCardKeyPosition.rightBottom]) { - + }
-
-
-
{{tickMax$ | async}}
-
{{tickMin$ | async}}
-
-
-
-
-
-
-
+ @if (showChart) { +
+
+
{{tickMax$ | async}}
+
{{tickMin$ | async}}
+
+
+
+
+
+
+
+
-
+ } -
+ @if (showDate) { +
+ }
-
- - arrow_upward - arrow_downward -
+ @if (value.showArrow) { +
+ @if (!value.upArrow && !value.downArrow) { + + } + @if (value.upArrow) { + arrow_upward + } + @if (value.downArrow) { + arrow_downward + } +
+ }
{{ value.value }} {{ value.units }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html index c5084fd21a..fd56942e6c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html @@ -23,7 +23,9 @@
- {{ icon }} + @if (showIcon) { + {{ icon }} + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html index 289b54bb56..ea20b3521b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html @@ -23,11 +23,17 @@
-
- {{ icon }} -
-
+ @if (showIcon || showLabel) { +
+ @if (showIcon) { + {{ icon }} + } + @if (showLabel) { +
+ } +
+ }
{{ valueText }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html index f486fd0594..e0a38d6eca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html @@ -21,13 +21,14 @@
widgets.notification.notification-types
- - {{ notificationTypesTranslateMap.get(type).name | translate }} - close - + @for (type of selectedNotificationTypes; track type) { + + {{ notificationTypesTranslateMap.get(type).name | translate }} + close + + } - - - + @for (type of filteredNotificationTypesList | async; track type) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html index 0b9acd06fd..9b99295ca8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html @@ -18,24 +18,30 @@
- + @if (layout === progressBarLayout.default) { - + }
-
- - - -
{{ valueText }}
-
+ @if (showTitleValueRow) { +
+ @if (layout === progressBarLayout.simplified) { + + } + @if (showValue) { +
{{ valueText }}
+ } +
+ }
-
-
{{ settings.tickMin }}
-
{{ settings.tickMax }}
-
+ @if (showTicks) { +
+
{{ settings.tickMin }}
+
{{ settings.tickMax }}
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html index 2cc7e023d2..2f9fdb4ca9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html @@ -18,38 +18,44 @@
-
- {{ count$ | async }} -
+ @if (showCounter) { +
+ {{ count$ | async }} +
+ }
- -
-
-
- - - -
-
-
-
- -
- - - -
- notification.no-notifications-yet -
- + @if (loadNotification) { + @if (notifications.length) { +
+
+ @for (notification of notifications; track notification.id.id; let last = $last) { +
+ + + @if (!last) { + + } +
+ } +
+
+ } @else { +
+ + + +
+ notification.no-notifications-yet + } + } @else {
notification.loading-notifications
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts index e68cddacf0..157dc0deec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts @@ -37,7 +37,7 @@ import { unreadNotificationDefaultSettings, UnreadNotificationWidgetSettings } from '@home/components/widget/lib/cards/unread-notification-widget.models'; -import { Notification, NotificationRequest, NotificationType } from '@shared/models/notification.models'; +import { Notification, NotificationType } from '@shared/models/notification.models'; import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.models'; import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; import { distinctUntilChanged, map, share, skip, take, tap } from 'rxjs/operators'; @@ -230,10 +230,6 @@ export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy { } } - trackById(index: number, item: NotificationRequest): string { - return item.id.id; - } - private editNotificationTypeFilter($event: Event) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html index 226b5668bf..6000ef6d3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html @@ -21,35 +21,35 @@
- - + @switch (layout) { + @case (valueCardLayout.square) { - - + } + @case (valueCardLayout.vertical) { - - + } + @case (valueCardLayout.centered) {
-
- + } + @case (valueCardLayout.simplified) { - - + } + @case (layout === valueCardLayout.horizontal || + layout === valueCardLayout.horizontal_reversed ? layout : '') {
-
-
+ } + }
@@ -62,13 +62,19 @@ - {{ icon }} + @if (showIcon) { + {{ icon }} + } -
{{ label$ | async }}
+ @if (showLabel) { +
{{ label$ | async }}
+ }
-
+ @if (showDate) { +
+ }
{{ valueText }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html index 8d276d1231..4950936073 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html @@ -19,9 +19,11 @@
-
-
{{ valueText }}
-
+ @if (showValue) { +
+
{{ valueText }}
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html index c8a7626cda..adbec193db 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html @@ -28,16 +28,20 @@
-
-
-
-
-
{{ legendKey.label }}
-
+ @if (showLegend) { +
+ @for (legendKey of legendKeys; track legendKey) { +
+
+
+
{{ legendKey.label }}
+
+
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html index 8e98d5f150..c840dc799e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html @@ -28,17 +28,21 @@
-
-
-
-
-
{{ legendItem.label }}
-
-
{{ legendItem.value }}
+ @if (showLegend) { +
+ @for (legendItem of legendItems; track legendItem) { +
+
+
+
{{ legendItem.label }}
+
+
{{ legendItem.value }}
+
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html index 48fe64d4a2..e001d9c56d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html @@ -28,14 +28,18 @@
-
-
-
-
-
{{ rangeItem.label }}
-
+ @if (showLegend) { +
+ @for (rangeItem of visibleRangeItems; track rangeItem) { +
+
+
+
{{ rangeItem.label }}
+
+
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html index a7d1d6eb6e..7ae4145eb6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html @@ -28,83 +28,107 @@
-
- - - - - -
+ @if (showLegend) { +
+ @if (displayLegendValues) { + @if (horizontalLegendPosition) { + + } @else { + + + + + @if (legendConfig.showMin === true) { + + } + @if (legendConfig.showMax === true) { + + } + @if (legendConfig.showAvg === true) { + + } + @if (legendConfig.showTotal === true) { + + } + @if (legendConfig.showLatest === true) { + + } + + + + @for (legendKey of legendKeys; track legendKey) { + + + @if (legendConfig.showMin === true) { + + } + @if (legendConfig.showMax === true) { + + } + @if (legendConfig.showAvg === true) { + + } + @if (legendConfig.showTotal === true) { + + } + @if (legendConfig.showLatest === true) { + + } + + } + +
{{ 'legend.Min' | translate }}{{ 'legend.Max' | translate }}{{ 'legend.Avg' | translate }}{{ 'legend.Total' | translate }}{{ 'legend.Latest' | translate }}
+ + + + + + +
+ } + } @else { + @for (legendKey of legendKeys; track legendKey) { + + } + } +
+ } - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - - - - - - - - - - + + + @for (legendKey of legendKeys; track legendKey) { + + } + - - - - - - - - + @if (legendConfig.showMin === true) { + + } + @if (legendConfig.showMax === true) { + + } + @if (legendConfig.showAvg === true) { + + } + @if (legendConfig.showTotal === true) { + + } + @if (legendConfig.showLatest === true) { + + }
{{ 'legend.Min' | translate }}{{ 'legend.Max' | translate }}{{ 'legend.Avg' | translate }}{{ 'legend.Total' | translate }}{{ 'legend.Latest' | translate }}
+ +
- - - - - - -
- - - - -
@@ -120,9 +144,11 @@ {{ label | translate }} - - + @for (legendKey of legendKeys; track legendKey) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html index 37eb1f807a..a607300147 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html @@ -20,20 +20,32 @@
-
-
-
+ @if (showIconBackground || showIcon) { +
+ @if (showIconBackground) { +
+
+
+ } + @if (showIcon) { + {{ icon }} + }
- {{ icon }} -
+ }
-
{{ label }}
-
+ @if (showLabel) { +
{{ label }}
+ } + @if (layout === countCardLayout.row) { +
+ }
{{ valueText }}
- chevron_right + @if (showChevron) { + chevron_right + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html index b3c95cc73b..6683fa8c4b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html @@ -16,20 +16,24 @@ -->
- - - + @if (settings.showTemplate) { + + + + } -
- - -
+ @if (!settings.autoConfirm) { +
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html index f5f43a5346..3b5990a268 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html @@ -20,7 +20,9 @@ [class.labels-hidden]="settings.hideLabels" [class.long-mode]="ctx.width >= 400"> - widgets.date-range-navigator.localizationMap.Date picker + @if (!settings.hideLabels) { + widgets.date-range-navigator.localizationMap.Date picker + } {{advancedModel.chosenLabel}} @@ -28,14 +30,20 @@ - widgets.date-range-navigator.localizationMap.Interval + @if (!settings.hideLabels) { + widgets.date-range-navigator.localizationMap.Interval + } - - {{'widgets.date-range-navigator.localizationMap.' + customInterval.label | translate}} - - - {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} - + @if (customInterval) { + + {{'widgets.date-range-navigator.localizationMap.' + customInterval.label | translate}} + + } + @for (date of datesMap | keyvalue: originalOrder; track date) { + + {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} + + } @@ -44,11 +52,15 @@ keyboard_arrow_left - widgets.date-range-navigator.localizationMap.Step size + @if (!settings.hideLabels) { + widgets.date-range-navigator.localizationMap.Step size + } - - {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} - + @for (date of datesMap | keyvalue: originalOrder; track date) { + + {{'widgets.date-range-navigator.localizationMap.' + date.value.label | translate}} + + } - +@if (addMode || editMode) { +
+ + @if (editMode) { +
+ + +
+ }
-
- - -
- - +} @else {
- +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html index 1f3756431c..fc2199b679 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html @@ -27,17 +27,19 @@
- - -
- - - - + @if (!editMode && !addMode) { + + } + @if (addMode) { + @switch (mode) { + @case ('docs') { - - + } + @case ('quickLinks') { + [ngModelOptions]="{standalone: true}" + (quickLinkAdded)="linkAdded($event)" + (quickLinkAddCanceled)="addMode = false"> - - - + } + } + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html index 582070ad33..5247c09ffd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html @@ -22,191 +22,213 @@ {{ index + 1 }} - - + @switch (authUser.authority) { + @case (authority.SYS_ADMIN) {
widgets.getting-started.sys-admin.step1.title
- {{ 'tenant.tenants' | translate }} + @if (matStepper.selectedIndex === 0) { + {{ 'tenant.tenants' | translate }} + }
description{{ 'widgets.getting-started.sys-admin.step1.how-to-create-tenant' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step2.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step2.how-to-configure-mail-server' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step3.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step3.how-to-configure-sms-provider' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step4.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step4.how-to-configure-2fa' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step5.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step5.how-to-configure-oauth2' | translate }} -
- - -
-
widgets.getting-started.sys-admin.step6.title
- {{ 'admin.settings' | translate }} -
-
-
- - description{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }} -
-
- - - -
-
widgets.getting-started.tenant-admin.step1.title
- {{ 'device.devices' | translate }} -
-
-
- - description{{ 'widgets.getting-started.tenant-admin.step1.how-to-create-device' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step2.title
-
-
-
-
- - Ubuntu - MacOS - Windows - - - -

+ + + +
+
widgets.getting-started.sys-admin.step2.title
+ @if (matStepper.selectedIndex === 1) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step2.how-to-configure-mail-server' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step3.title
+ @if (matStepper.selectedIndex === 2) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step3.how-to-configure-sms-provider' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step4.title
+ @if (matStepper.selectedIndex === 3) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step4.how-to-configure-2fa' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step5.title
+ @if (matStepper.selectedIndex === 4) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step5.how-to-configure-oauth2' | translate }} +
+ + +
+
widgets.getting-started.sys-admin.step6.title
+ @if (matStepper.selectedIndex === 5) { + {{ 'admin.settings' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }} +
+ } + @case (authority.TENANT_ADMIN) { + + +
+
widgets.getting-started.tenant-admin.step1.title
+ @if (matStepper.selectedIndex === 0) { + {{ 'device.devices' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step1.how-to-create-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step2.title
+
+
+
+
+ + Ubuntu + MacOS + Windows + + @switch (publishCommandSteps.value) { + @case ('ubuntu') { +

- - -

+ } + @case ('macos') { +

-
- -

-
- -

+ } + @case ('windows') { +

+ } + } +

-
-
- - description{{ 'widgets.getting-started.tenant-admin.step2.how-to-connect-device' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step3.title
- {{ 'dashboard.dashboards' | translate }} -
-
-
- - description{{ 'widgets.getting-started.tenant-admin.step3.how-to-create-dashboard' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step4.title
- {{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }} -
-
-
- - description{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step5.title
-
-
-
-

+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step2.how-to-connect-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step3.title
+ @if (matStepper.selectedIndex === 2) { + {{ 'dashboard.dashboards' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step3.how-to-create-dashboard' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step4.title
+ @if (matStepper.selectedIndex === 3) { + {{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step5.title
+
+
+
+

-
- - description{{ 'widgets.getting-started.tenant-admin.step5.how-to-create-alarm' | translate }} -
- - -
-
widgets.getting-started.tenant-admin.step6.title
- {{ 'customer.customers' | translate }} -
-
-
- - description{{ 'widgets.getting-started.tenant-admin.step6.how-to-create-customer-and-assign-dashboard' | translate }} -
-
-
- -
- -
- +
+ + description{{ 'widgets.getting-started.tenant-admin.step5.how-to-create-alarm' | translate }} + + + +
+
widgets.getting-started.tenant-admin.step6.title
+ @if (matStepper.selectedIndex === 5) { + {{ 'customer.customers' | translate }} + } +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step6.how-to-create-customer-and-assign-dashboard' | translate }} +
+ } + } + + @if (allCompleted) { +
+ +
+ } + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html index 9fa8ea6b13..6692c97e46 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html @@ -15,52 +15,69 @@ limitations under the License. --> -
-
-
-
-
- - widgets.version-info.available-version - widgets.version-info.available - + @if (updateMessage?.updateAvailable) { +
+ -
{{ updateMessage?.latestVersion }}
- -
- {{ 'widgets.version-info.upgrade' | translate }} + } @else { +
+
check
+
widgets.version-info.version-is-up-to-date
-
+ }
- -
-
check
-
widgets.version-info.version-is-up-to-date
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html index 025dbf3e33..c47fd94f75 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html @@ -24,21 +24,26 @@
-
-
+ @if (solid) { +
+
+ } @else { + @for (section of batterySections; track trackBySection($index)) { +
+ } + }
-
-
{{ valueText }}
-
+ @if (showValue) { +
+
{{ valueText }}
+
+ }
- -
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html index 9c2803d117..d9cf483025 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html @@ -20,11 +20,15 @@
-
-
-
- {{ error }} + @if (errorsMsg.length) { +
+
+ @for (error of errorsMsg; track error) { +
+ {{ error }} +
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html index 0dbd05d6bc..43c92c9c73 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html @@ -17,25 +17,35 @@ -->
-
-
- -
+
+ @if (widgetComponent.dashboardWidget.showWidgetTitlePanel || showDate) { +
+ + @if (showDate) { +
+ }
-
-
-
-
-
-
-
widgets.signal-strength.value
-
{{ tooltipValueText }}
-
-
-
widgets.signal-strength.last-update
-
-
+ } +
+
+
+ @if (showTooltip) { +
+
+ @if (showTooltipValue) { +
+
widgets.signal-strength.value
+
{{ tooltipValueText }}
+ } + @if (showTooltipDate) { +
+
widgets.signal-strength.last-update
+
+
+ }
-
+
+ } +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html index 0cc88f5404..29e51327db 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html @@ -25,9 +25,15 @@ {{ icon }}
-
{{ label$ | async }}
-
{{ status$ | async }}
+ @if (showLabel) { +
{{ label$ | async }}
+ } + @if (showStatus) { +
{{ status$ | async }}
+ }
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html index e12bb74d9e..42786f7a58 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html @@ -16,43 +16,45 @@ -->
-
-
-
- -
-
- - + @if (attributeUpdateFormGroup) { + + @if (datasourceDetected && !errorMessage) { +
+
+ +
+
+ + +
-
- - -
- {{ errorMessage | translate }} -
-
- + } @else { +
+ {{ errorMessage | translate }} +
+ } + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html index cd50cb6174..4c74e0e668 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html @@ -17,72 +17,120 @@ --> - - - - - - - - + @if (!isRowDirection) { + + + @if (legendConfig.showMin === true) { + + } + @if (legendConfig.showMax === true) { + + } + @if (legendConfig.showAvg === true) { + + } + @if (legendConfig.showTotal === true) { + + } + @if (legendConfig.showLatest === true) { + + } + + } - - - - - + + @if (legendConfig.showMin === true) { + - - - - + } + + } + } + @if (isRowDirection) { + + + @for (legendKey of legendKeys(); track legendKey) { + + } - - - - - - - - + @for (legendKey of legendKeys(); track legendKey) { + - - - - + + @for (legendKey of legendKeys(); track legendKey) { + - - - - + + @for (legendKey of legendKeys(); track legendKey) { + - - - - + + @for (legendKey of legendKeys(); track legendKey) { + - - - - + + @for (legendKey of legendKeys(); track legendKey) { + - + } + + }
{{ 'legend.min' | translate }}{{ 'legend.max' | translate }}{{ 'legend.avg' | translate }}{{ 'legend.total' | translate }}{{ 'legend.latest' | translate }}
{{ 'legend.min' | translate }}{{ 'legend.max' | translate }}{{ 'legend.avg' | translate }}{{ 'legend.total' | translate }}{{ 'legend.latest' | translate }}
- + +
+ + + +
- - - -
{{ 'legend.min' | translate }} + {{ 'legend.min' | translate }}
{{ 'legend.max' | translate }} + } + @if (isRowDirection && legendConfig.showMax === true) { +
{{ 'legend.max' | translate }}
{{ 'legend.avg' | translate }} + } + @if (isRowDirection && legendConfig.showAvg === true) { +
{{ 'legend.avg' | translate }}
{{ 'legend.total' | translate }} + } + @if (isRowDirection && legendConfig.showTotal === true) { +
{{ 'legend.total' | translate }}
{{ 'legend.latest' | translate }} + } + @if (isRowDirection && legendConfig.showLatest === true) { +
{{ 'legend.latest' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html index 485141bb03..813d32609f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html @@ -23,16 +23,22 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
entity.entity - - {{ entity.entityParseName }} - + @for (entity of data.entities; track entity) { + + {{ entity.entityParseName }} + + }
widgets.maps.select-entity-hint
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html index 870583125b..c85272dd90 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.html @@ -25,7 +25,9 @@
-
+ @if (settings.showTimestamp) { +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html index 1da11e665e..d407ba95e0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html @@ -19,9 +19,11 @@ {{ (selectEntityFormGroup.get('entity').value ? 'entity.entity' : 'widgets.maps.data-layer.select-entity') | translate }} - - {{ entity.entity.entityDisplayName }} - + @for (entity of entities; track entity) { + + {{ entity.entity.entityDisplayName }} + + } widgets.maps.data-layer.select-entity-hint diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index 4cfc970feb..a5be42db25 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -26,21 +26,29 @@ [class.row]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.RIGHT" [class.row-reverse]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.LEFT">
-
- - {{ 'widgets.mobile-app-qr-code.download-on-the-app-store' | translate }} - - - {{ 'widgets.mobile-app-qr-code.get-it-on-google-play' | translate }} - + @if ((mobileAppSettings?.iosEnabled || mobileAppSettings?.androidEnabled) && + mobileAppSettings?.qrCodeConfig.badgeEnabled && showBadgeContainer) { +
+ @if (mobileAppSettings?.iosEnabled) { + + {{ 'widgets.mobile-app-qr-code.download-on-the-app-store' | translate }} + + } + @if (mobileAppSettings?.androidEnabled) { + + {{ 'widgets.mobile-app-qr-code.get-it-on-google-play' | translate }} + + } +
+ } +
+ @if (mobileAppSettings?.qrCodeConfig.qrCodeLabelEnabled) { +
+ {{ mobileAppSettings.qrCodeConfig.qrCodeLabel }} +
+ }
-
- {{ mobileAppSettings.qrCodeConfig.qrCodeLabel }} -
-
- diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html index c03ac0c603..9f6b755431 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -19,218 +19,281 @@ [formGroup]="multipleInputFormGroup" tb-toast toastTarget="{{ toastTargetId }}" (ngSubmit)="saveForm()" novalidate autocomplete="off"> -
-
- {{ getGroupTitle(source.datasource) }} - -
- - - {{key.label}} - - - - - - {{ getErrorMessageText(key.settings, 'required') }} - - - - {{key.label}} - - - - - - {{ getErrorMessageText(key.settings,'required') }} - - - {{ getErrorMessageText(key.settings,'min') }} - - - {{ getErrorMessageText(key.settings,'max') }} - - - - {{key.label}} - - - - - - - {{ getErrorMessageText(key.settings,'required') }} - - - {{ getErrorMessageText(key.settings,'invalidJSON') | translate }} - - - - {{key.label}} - - - - - {{ getErrorMessageText(key.settings, 'required') }} - - - {{ getErrorMessageText(key.settings, 'invalidDate') }} - - - - {{key.label}} - - - {{ getCustomTranslationText(option.label ? option.label : option.value) }} - - - - - - - {{ getErrorMessageText(key.settings, 'required') }} - - - -
- {{key.label}} - - - + @if (entityDetected && isAllParametersValid) { +
+ @for (source of sources; track source) { +
+ @if (settings.showGroupTitle) { + {{ getGroupTitle(source.datasource) }} + + } +
+ @for (key of visibleKeys(source); track key) { + @if (key.settings.dataKeyValueType === 'string') { + + {{key.label}} + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings, 'required') }} + + } + + } + @if (['double', 'integer'].includes(key.settings.dataKeyValueType)) { + + {{key.label}} + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings,'required') }} + + } + @if (multipleInputFormGroup.get(key.formId).hasError('min')) { + + {{ getErrorMessageText(key.settings,'min') }} + + } + @if (multipleInputFormGroup.get(key.formId).hasError('max')) { + + {{ getErrorMessageText(key.settings,'max') }} + + } + + } + @if (key.settings.dataKeyValueType === 'JSON') { + + {{key.label}} + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + + @if (multipleInputFormGroup.get(key.formId).hasError('required') && !multipleInputFormGroup.get(key.formId).hasError('invalidJSON')) { + + {{ getErrorMessageText(key.settings,'required') }} + + } + @if (multipleInputFormGroup.get(key.formId).hasError('invalidJSON')) { + + {{ getErrorMessageText(key.settings,'invalidJSON') | translate }} + + } + + } + @if (['dateTime','date', 'time'].includes(key.settings.dataKeyValueType)) { + + {{key.label}} + + + + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings, 'required') }} + + } + @if (multipleInputFormGroup.get(key.formId).hasError('matDatepickerParse')) { + + {{ getErrorMessageText(key.settings, 'invalidDate') }} + + } + + } + @if (key.settings.dataKeyValueType === 'select') { + + {{key.label}} + + @for (option of key.settings.selectOptions; track option) { + + {{ getCustomTranslationText(option.label ? option.label : option.value) }} + + } + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings, 'required') }} + + } + + } + @if (key.settings.dataKeyValueType === 'radio') { +
+ {{key.label}} + + @for (option of key.settings.selectOptions; track option) { + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + + {{ getCustomTranslationText(option.label ? option.label : option.value) }} + + + } + +
+ } + @if (key.settings.dataKeyValueType === 'color') { + + {{key.label}} + + @if (key.settings.icon || key.settings.customIconUrl) { + + + + } + + + @if (multipleInputFormGroup.get(key.formId).hasError('required')) { + + {{ getErrorMessageText(key.settings, 'required') }} + + } + + } + @if (key.settings.dataKeyValueType === 'booleanCheckbox') { + + @if (key.settings.icon || key.settings.customIconUrl) { + + } + {{key.label}} + + } + @if (key.settings.dataKeyValueType === 'booleanSwitch') { + + @if (key.settings.icon || key.settings.customIconUrl) { - - - {{ getCustomTranslationText(option.label ? option.label : option.value) }} - - - + } + {{key.label}} + + } + }
- - {{key.label}} - - - - - - - - {{ getErrorMessageText(key.settings, 'required') }} - - - - - - - {{key.label}} - - - - - - {{key.label}} - - -
-
-
-
- - -
-
-
- {{ 'widgets.input-widgets.no-entity-selected' | translate }} + + }
-
- {{ 'widgets.input-widgets.not-allowed-entity' | translate }} + } + @if (entityDetected && isAllParametersValid && settings.showActionButtons) { +
+ + +
+ } + @if ((!entityDetected || !isAllParametersValid) && datasourceDetected) { +
+
+ {{ 'widgets.input-widgets.no-entity-selected' | translate }} +
+
+ {{ 'widgets.input-widgets.not-allowed-entity' | translate }} +
-
+ } - {{key.settings.icon}} - + @if (!key.settings.customIconUrl) { + {{key.settings.icon}} + } @else { icon - + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html index 2ca0650502..2f1f53968e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html @@ -15,22 +15,28 @@ limitations under the License. --> - - - - - {{section.name}} - - - - - - {{place.icon}} - {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} - - - - - - - +@if (cols) { + + @for (section of showHomeSections$| async; track section) { + + + + {{section.name}} + + + + @for (place of sectionPlaces(section); track place) { + + + {{place.icon}} + {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} + + + } + + + + + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html index 36b7d038b0..6b1d18fe43 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html @@ -16,50 +16,56 @@ -->
-
-
-
- widgets.input-widgets.no-image - last photo -
- -
-
-
- -
-
- - - -
+ @if (isEntityDetected && dataKeyDetected && isCameraSupport && isProtocolHttps) { +
+
+
+ widgets.input-widgets.no-image + last photo
-
-
- preview photo - -
-
- - -
+ } +
+
+
+ +
+
+ + + +
+
+
+
+ preview photo + +
+
+ + +
+
-
-
- {{ textMessage | translate }} -
+ } + @if (textMessage) { +
+ {{ textMessage | translate }} +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html index 01313aa0d0..87495e0477 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html @@ -17,6 +17,10 @@ -->
-
entity.no-data
-
widgets.invalid-qr-code-text
+ @if (!qrCodeText && !invalidQrCodeText) { +
entity.no-data
+ } + @if (invalidQrCodeText) { +
widgets.invalid-qr-code-text
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html index 1b5b3f1992..f8d8b7c0e3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
@@ -39,12 +43,16 @@ widgets.persistent-table.method - - {{'widgets.persistent-table.method-error' | translate}} - - - {{'widgets.persistent-table.white-space-error' | translate}} - + @if (persistentFormGroup.get('method').hasError('required')) { + + {{'widgets.persistent-table.method-error' | translate}} + + } + @if (persistentFormGroup.get('method').hasError('pattern')) { + + {{'widgets.persistent-table.white-space-error' | translate}} + + } widgets.persistent-table.retries @@ -71,7 +79,9 @@
-
+ @if (!(isLoading$ | async)) { +
+ }
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
@@ -55,11 +59,13 @@ widgets.persistent-table.method - - widgets.persistent-table.retries - - + @if (persistentFormGroup.get('retries').value) { + + widgets.persistent-table.retries + + + }
@@ -69,7 +75,7 @@ {{ 'widgets.persistent-table.response' | translate }} - + @@ -98,14 +104,15 @@
- + @if (allowDelete) { + + }
- -
-
- - - - - - + }
+ @if (actionCellButtonAction.length) { +
+ + + @for (actionDescriptor of actionCellButtonAction; track actionDescriptor) { + @if (actionDescriptor.icon) { + + } + } + +
+ } + [class.!hidden]="persistentDatasource.dataLoading"> {{ noDataDisplayMessageText }} + class="no-data-found flex items-center justify-center">{{ noDataDisplayMessageText }} {{ 'common.loading' | translate }} -
- - + class="no-data-found flex items-center justify-center">{{ 'common.loading' | translate }} +
+ @if (displayPagination) { + + } + @if (displayPagination) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html index faf054957c..83dd2c5b3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html @@ -22,5 +22,7 @@
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html index b54bc91b6f..421668d0f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html @@ -21,20 +21,32 @@
-
- {{ icon }} -
{{ label$ | async }}
-
+ @if (showIcon || showLabel) { +
+ @if (showIcon) { + {{ icon }} + } + @if (showLabel) { +
{{ label$ | async }}
+ } +
+ }
-
{{ offLabel }}
+ @if (showOffLabel) { +
{{ offLabel }}
+ } -
{{ onLabel }}
+ @if (showOnLabel) { +
{{ onLabel }}
+ }
- + @if (loading$ | async) { + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html index 60523571fd..afd2bb085c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html @@ -17,18 +17,24 @@ -->
-
- -
-
-
-
{{ valueText }}
+ @if (showWidgetTitlePanel) { +
+
-
-
- {{ leftIcon }} + } +
+ @if (showValue) { +
+
{{ valueText }}
+ } +
+ @if (showLeftRightIcon) { +
+ {{ leftIcon }} +
+ }
-
-
-
{{ tickMinText }}
-
-
-
{{ tickMaxText }}
+ @if (showTicks) { +
+
+
{{ tickMinText }}
+
+
+
{{ tickMaxText }}
+
-
-
-
- {{ rightIcon }} + }
+ @if (showLeftRightIcon) { +
+ {{ rightIcon }} +
+ }
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.html index d681981e01..4eef6a39b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.html @@ -15,40 +15,45 @@ limitations under the License. --> -
-
- {{ error }} -
-
- {{title}} -
-
+
+ {{ error }} +
+
+ {{title}} +
+
-
- - -
-
- OFF - - ON - + id="switch-container"> +
+ + +
+
+ OFF + + ON + +
+
-
-
- +} @else {
{{title}} - -
+ @if (executingUpdateValue) { + + } + @if (!executingUpdateValue) { +
+ } + [error]="error">
-
+} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html index d70dda0019..c9c24439ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html @@ -31,7 +31,7 @@
{{ valueText }}
+ [style]="valueStyle">{{ valueText }}
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html index 830a62b484..6a0acbdf25 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html @@ -17,13 +17,17 @@ -->
- + @if (widgetComponent.dashboardWidget.showWidgetTitlePanel) { - + }
-
scada.no-symbol-selected
+ @if (noScadaSymbol) { +
scada.no-symbol-selected
+ }
- + @if (loading$ | async) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index e81eed3b4a..64183bd00b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -101,34 +101,36 @@ - - warning - + class="tb-error"> + warning + + }
widgets.table.page-step-count
- - warning - + class="tb-error"> + warning + + }
@@ -136,9 +138,11 @@
widgets.table.default-page-size
- - {{ size }} - + @for (size of pageStepSizeValues; track size) { + + {{ size }} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html index fc89c9840e..82f1de831d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html @@ -29,7 +29,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.power-button.power-on
@@ -38,7 +38,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="onUpdateState"> + formControlName="onUpdateState">
widgets.power-button.power-off
@@ -47,7 +47,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="offUpdateState"> + formControlName="offUpdateState">
widgets.rpc-state.disabled-state
@@ -58,7 +58,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -69,12 +69,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout"> - - {{ powerButtonLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout"> + @for (layout of powerButtonLayouts; track layout) { + + {{ powerButtonLayoutTranslationMap.get(layout) | translate }} + + }
{{ 'widgets.background.background' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html index fc27e782e2..e6331aded8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html @@ -29,7 +29,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
@@ -60,7 +60,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -72,12 +72,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout"> - - {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout"> + @for (layout of segmentedButtonLayouts; track layout) { + + {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} + + }
@@ -216,16 +218,18 @@ -
-
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
- - -
+ @for (state of widgetButtonToggleStates; track state) { +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+ }
@@ -257,16 +261,18 @@ -
-
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
- - -
+ @for (state of widgetButtonToggleStates; track state) { +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html index 2b579b4750..45dcb56a5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html @@ -22,9 +22,11 @@
{{ 'widgets.aggregated-value-card.position' | translate }}
- - {{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} - + @for (position of aggregatedValueCardKeyPositions; track position) { + + {{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html index 046d76d351..a718b11669 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html @@ -38,31 +38,37 @@
widgets.api-usage.current-number
-
-
-
- - -
- + @if (dataKeysFormArray().controls?.length) { +
+ @for (dataKeyControl of dataKeysFormArray().controls; track trackByDataKey($index)) { +
+
+ + +
+ +
+
-
+ }
-
+ } @else { + {{ 'widgets.api-usage.no-key' | translate }} + }
- - {{ 'widgets.api-usage.no-key' | translate }} - - + @if (dashboardStateWidgetSettingsForm.get('stateId').value) { + + } - - - + @for (state of filteredDashboardStates | async; track state) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html index ae477eeb6d..767be11007 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html @@ -24,13 +24,15 @@ - + @if (!disabled) { + + } @@ -40,9 +42,11 @@ widgets.label-widget.label-pattern - - {{ 'widgets.label-widget.label-pattern-required' | translate }} - + @if (labelWidgetLabelFormGroup.get('pattern').hasError('required')) { + + {{ 'widgets.label-widget.label-pattern-required' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html index 7dad480482..52e3f39fa3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html @@ -17,27 +17,30 @@ -->
+ label="{{ 'widgets.label-widget.background-image' | translate }}" + formControlName="backgroundImageUrl">
widgets.label-widget.labels
-
- - -
-
-
- widgets.label-widget.no-labels + @for (labelControl of typedSelectOptions; track trackByLabelControl($index, labelControl); let isLast = $last) { +
+ + +
+ }
+ @if (!labelsFormArray().controls.length) { +
+ widgets.label-widget.no-labels +
+ }
-
-
-
widget-config.card-style
-
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
-
-
- - {{ 'admin.mobile-app.badges' | translate }} - - - - - {{ badgePosition.value | translate }} - - - + @if (!mobileAppQRCodeWidgetSettingsForm.get('useSystemSettings').value) { +
+
+
widget-config.card-style
+
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
+
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + @for (badgePosition of badgePositionTranslationsMap | keyvalue; track badgePosition) { + + {{ badgePosition.value | translate }} + + } + + +
+
+
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + +
-
-
- - {{ 'admin.mobile-app.label' | translate }} - - - - +
+
+
{{ 'widgets.background.background' | translate }}
+ +
-
-
-
{{ 'widgets.background.background' | translate }}
- - +
{{ 'widget-config.card-padding' | translate }}
+ + +
-
-
{{ 'widget-config.card-padding' | translate }}
- - - -
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html index bd16e4a467..9c222e2379 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html @@ -23,12 +23,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout"> - - {{ progressBarLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout"> + @for (layout of progressBarLayouts; track layout) { + + {{ progressBarLayoutTranslationMap.get(layout) | translate }} + + }
@@ -63,20 +65,22 @@
-
- - {{ 'widgets.progress-bar.range-ticks' | translate }} - -
- - - - + @if (progressBarWidgetSettingsForm.get('layout').value === progressBarLayout.default) { +
+ + {{ 'widgets.progress-bar.range-ticks' | translate }} + +
+ + + + +
-
+ }
{{ 'widgets.progress-bar.bar-color' | translate }}
widgets.qr-code.qr-code-text-pattern {{ 'widgets.qr-code.qr-code-text-pattern-hint' | translate }} - - {{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }} - + @if (qrCodeWidgetSettingsForm.get('qrCodeTextPattern').hasError('required')) { + + {{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }} + + } - - warning - + class="tb-error"> + warning + + }
widgets.table.page-step-count
- - warning - + class="tb-error"> + warning + + }
@@ -103,9 +105,11 @@
widgets.table.default-page-size
- - {{ size }} - + @for (size of pageStepSizeValues; track size) { + + {{ size }} + + } @@ -115,23 +119,23 @@ {{ 'widgets.table.use-entity-label-tab-name' | translate }} -
-
widgets.table.sort-by
-
- - - {{ entityFields.createdTime.name | translate }} - {{ entityFields.name.name | translate }} - - - - - {{ 'common.sort-asc' | translate }} - {{ 'common.sort-desc' | translate }} - - -
-
+
+
widgets.table.sort-by
+
+ + + {{ entityFields.createdTime.name | translate }} + {{ entityFields.name.name | translate }} + + + + + {{ 'common.sort-asc' | translate }} + {{ 'common.sort-desc' | translate }} + + +
+
widgets.table.rows
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html index 15832b67a3..e605e97c89 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html @@ -23,12 +23,14 @@ breakpoints: { 'lt-md': horizontal ? 1 : 2 }}" - label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout"> - - {{ valueCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout"> + @for (layout of valueCardLayouts; track layout) { + + {{ valueCardLayoutTranslationMap.get(layout) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html index 1f042ab1ff..a03ec60e35 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html @@ -23,12 +23,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout"> - - {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout"> + @for (layout of valueChartCardLayouts; track layout) { + + {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} + + }
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 8ab61ea509..bda194828e 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 @@ -24,7 +24,7 @@
+ formControlName="grid">
widgets.bar-chart.bar-appearance
@@ -78,43 +78,43 @@
+ formControlName="barBackgroundSettings" + titleText="widgets.chart.background" + fillNoneTitle="widgets.chart.fill-type-solid"> + stroked + formControlName="noAggregationBarWidthSettings">
widgets.time-series-chart.axis.y-axis
+ formControlName="yAxis" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + axisType="yAxis" + advanced + hideUnits + hideDecimals>
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + axisType="xAxis" + advanced>
+ formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="dataKeyCallbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + hideYAxis>
@@ -130,9 +130,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -193,7 +195,7 @@
+ formControlName="tooltipDateFormat"> @@ -229,7 +231,7 @@
+ formControlName="animation">
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html index 87a84493c2..9bbfc28921 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html @@ -202,16 +202,19 @@ widgets.chart.add-threshold
-
-
- - + @if (thresholdsFormArray().length) { +
+ @for (thresholdControl of thresholdsFormArray().controls; track trackByThreshold($index, thresholdControl)) { +
+ + +
+ }
-
+ }
widgets.chart.comparison-settings
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html index b458e20387..a9034b9e12 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-pie-widget-settings.component.html @@ -22,23 +22,29 @@ widgets.chart.radius - - {{ 'widgets.chart.common-pie-settings-range-error' | translate }} - + @if (flotPieWidgetSettingsForm.get('radius').hasError('min') || flotPieWidgetSettingsForm.get('radius').hasError('max')) { + + {{ 'widgets.chart.common-pie-settings-range-error' | translate }} + + } widgets.chart.inner-radius - - {{ 'widgets.chart.common-pie-settings-range-error' | translate }} - + @if (flotPieWidgetSettingsForm.get('innerRadius').hasError('min') || flotPieWidgetSettingsForm.get('innerRadius').hasError('max')) { + + {{ 'widgets.chart.common-pie-settings-range-error' | translate }} + + } widgets.chart.tilt - - {{ 'widgets.chart.common-pie-settings-range-error' | translate }} - + @if (flotPieWidgetSettingsForm.get('tilt').hasError('min') || flotPieWidgetSettingsForm.get('tilt').hasError('max')) { + + {{ 'widgets.chart.common-pie-settings-range-error' | translate }} + + } @@ -56,9 +62,9 @@ + formControlName="color" + icon="format_color_fill" + label="{{ 'widgets.chart.color' | translate }}" openOnInput colorClearButton>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html index e97d45fdd1..ad8b865391 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html @@ -18,13 +18,14 @@
- - - - - - + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html index 0ae52d9993..2079d5e35f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html @@ -24,43 +24,53 @@ {{ 'widgets.chart.enable-selection-mode' | translate }} - - {{ 'widgets.chart.display-smooth-lines' | translate }} - -
-
widgets.chart.line-shadow-size
- - - -
-
-
widgets.chart.default-bar-width
- - - -
-
-
{{ 'widgets.chart.bar-alignment' | translate }}
- - - - {{ 'widgets.chart.bar-alignment-left' | translate }} - - - {{ 'widgets.chart.bar-alignment-right' | translate }} - - - {{ 'widgets.chart.bar-alignment-center' | translate }} - - - -
-
-
widgets.chart.thresholds-line-width
- - - -
+ @if (chartType === 'graph') { + + {{ 'widgets.chart.display-smooth-lines' | translate }} + + } + @if (chartType === 'graph') { +
+
widgets.chart.line-shadow-size
+ + + +
+ } + @if (chartType === 'bar') { +
+
widgets.chart.default-bar-width
+ + + +
+ } + @if (chartType === 'bar') { +
+
{{ 'widgets.chart.bar-alignment' | translate }}
+ + + + {{ 'widgets.chart.bar-alignment-left' | translate }} + + + {{ 'widgets.chart.bar-alignment-right' | translate }} + + + {{ 'widgets.chart.bar-alignment-center' | translate }} + + + +
+ } + @if (chartType === 'graph' || chartType === 'bar') { +
+
widgets.chart.thresholds-line-width
+ + + +
+ }
{{ 'widgets.chart.default-font' | translate }}
@@ -74,25 +84,27 @@
-
-
widget-config.legend
- - - - - {{ 'widget-config.legend' | translate }} - - - - widget-config.advanced-settings - - - - - - - -
+ @if (chartType === 'graph' || chartType === 'bar') { +
+
widget-config.legend
+ + + + + {{ 'widget-config.legend' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+ }
widgets.chart.axis
@@ -267,124 +279,131 @@
-
-
widgets.chart.comparison-settings
- - - - - {{ 'widgets.chart.enable-comparison' | translate }} - - - - widget-config.advanced-settings - - - -
-
{{ 'widgets.chart.time-for-comparison' | translate }}
- - - - {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - - {{ 'widgets.chart.time-for-comparison-days' | translate }} - - - {{ 'widgets.chart.time-for-comparison-weeks' | translate }} - - - {{ 'widgets.chart.time-for-comparison-months' | translate }} - - - {{ 'widgets.chart.time-for-comparison-years' | translate }} - - - {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} - - - -
-
-
widgets.chart.custom-interval-value
- - - -
-
-
widgets.chart.comparison-x-axis-settings
-
-
widgets.chart.axis-title
- - - -
-
- - {{ 'widgets.chart.show-tick-labels' | translate }} + @if (chartType === 'graph' || chartType === 'bar') { +
+
widgets.chart.comparison-settings
+ + + + + {{ 'widgets.chart.enable-comparison' | translate }} -
+ + + widget-config.advanced-settings + + +
-
{{ 'widgets.chart.axis-position' | translate }}
+
{{ 'widgets.chart.time-for-comparison' | translate }}
- - - {{ 'widgets.chart.axis-position-top' | translate }} + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - {{ 'widgets.chart.axis-position-bottom' | translate }} + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }}
-
- - -
-
-
widgets.chart.custom-legend-settings
- - - - - {{ 'widgets.chart.enable-custom-legend' | translate }} - - - - widget-config.advanced-settings - - - -
-
widgets.chart.label-keys-list
-
-
-
- - -
+
+
widgets.chart.custom-interval-value
+ + + +
+
+
widgets.chart.comparison-x-axis-settings
+
+
widgets.chart.axis-title
+ + +
-
- widgets.chart.no-label-keys +
+ + {{ 'widgets.chart.show-tick-labels' | translate }} +
-
- +
+
{{ 'widgets.chart.axis-position' | translate }}
+ + + + {{ 'widgets.chart.axis-position-top' | translate }} + + + {{ 'widgets.chart.axis-position-bottom' | translate }} + + +
-
- - -
+ + +
+ } + @if (chartType === 'graph' || chartType === 'bar') { +
+
widgets.chart.custom-legend-settings
+ + + + + {{ 'widgets.chart.enable-custom-legend' | translate }} + + + + widget-config.advanced-settings + + + +
+
widgets.chart.label-keys-list
+
+
+ @for (labelDataKeyControl of typedSelectOptions; track trackByLabelDataKey($index, labelDataKeyControl); let isLast = $last) { +
+ + +
+ } +
+ @if (!dataKeysListForLabelsFormArray().controls.length) { +
+ widgets.chart.no-label-keys +
+ } +
+ +
+
+
+
+
+
+ } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts index 65fc17df26..4b3af4a63e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts @@ -424,6 +424,10 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit return this.flotSettingsFormGroup.get('dataKeysListForLabels') as UntypedFormArray; } + get typedSelectOptions() { + return this.dataKeysListForLabelsFormArray().controls as (AbstractControl & { new?: boolean })[]; + } + public trackByLabelDataKey(index: number, labelDataKeyControl: AbstractControl): any { return labelDataKeyControl; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html index e9ca4d51ea..9be7ec3da7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html @@ -24,13 +24,15 @@
- + @if (!disabled) { + + }
@@ -41,9 +43,11 @@ widgets.chart.key-name - - {{ 'widgets.chart.key-name-required' | translate }} - + @if (labelDataKeyFormGroup.get('name').hasError('required')) { + + {{ 'widgets.chart.key-name-required' | translate }} + + } widgets.chart.key-type 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 8d7ba9a603..fcc637f7b5 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 @@ -31,9 +31,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -84,9 +86,11 @@
- - {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} - + @for (type of latestChartTooltipValueTypes; track type) { + + {{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} + + } @@ -120,7 +124,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -149,9 +153,11 @@
- - {{ pieChartLabelPositionTranslationMap.get(position) | translate }} - + @for (position of pieChartLabelPositions; track position) { + + {{ pieChartLabelPositionTranslationMap.get(position) | translate }} + + } - - {{ doughnutLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.doughnut.layout' | translate }}" formControlName="layout"> + @for (layout of doughnutLayouts; track layout) { + + {{ doughnutLayoutTranslationMap.get(layout) | translate }} + + }
@@ -256,8 +264,8 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
@@ -303,9 +311,9 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
@@ -360,9 +368,11 @@
{{ 'widgets.radar-chart.shape' | translate }}
- - {{ radarChartShapeTranslations.get(shape) | translate }} - + @for (shape of radarChartShapes; track shape) { + + {{ radarChartShapeTranslations.get(shape) | translate }} + + }
@@ -380,9 +390,11 @@
- - {{ chartLineTypeTranslations.get(type) | translate }} - + @for (type of chartLineTypes; track type) { + + {{ chartLineTypeTranslations.get(type) | translate }} + + } @@ -398,9 +410,11 @@
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + } @@ -416,9 +430,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + }
+ formControlName="fillAreaSettings">
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 ded6a1a7b4..2ff773c61c 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 @@ -24,7 +24,7 @@
+ formControlName="grid">
widgets.range-chart.range-chart-appearance
@@ -45,11 +45,11 @@ {{ 'widgets.range-chart.show-range-thresholds' | translate }} + boxButton + hideYAxis + [widgetConfig]="widgetConfig?.config" + title="widgets.range-chart.range-thresholds-settings" + formControlName="rangeThreshold">
@@ -77,9 +77,11 @@ - - {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} - + @for (stepType of lineSeriesStepTypes; track stepType) { + + {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} + + }
@@ -92,9 +94,11 @@
widgets.time-series-chart.line-type
- - {{ chartLineTypeTranslations.get(lineType) | translate }} - + @for (lineType of chartLineTypes; track lineType) { + + {{ chartLineTypeTranslations.get(lineType) | translate }} + + }
@@ -121,9 +125,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + } widgets.time-series-chart.series.point.point-shape
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + }
@@ -168,29 +176,29 @@
widgets.time-series-chart.axis.y-axis
+ formControlName="yAxis" + [aliasController]="aliasController" + [dataKeyCallbacks]="callbacks" + [datasource]="datasource" + axisType="yAxis" + hideUnits + hideDecimals>
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + axisType="xAxis">
+ formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="dataKeyCallbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + hideYAxis>
@@ -206,9 +214,11 @@
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslationMap.get(pos) | translate }} - + @for (pos of legendPositions; track pos) { + + {{ legendPositionTranslationMap.get(pos) | translate }} + + }
@@ -269,7 +279,7 @@
+ formControlName="tooltipDateFormat"> @@ -305,7 +315,7 @@
+ formControlName="animation">
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html index fcc2faf05f..d8706aa2d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html @@ -34,67 +34,74 @@
widgets.time-series-chart.axis.y-axis
- - {{ yAxis }} - + @for (yAxis of yAxisIds; track yAxis) { + + {{ yAxis }} + + }
-
-
widgets.time-series-chart.series.series-type
- - {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} - -
+ @if (chartType === TimeSeriesChartType.default) { +
+
widgets.time-series-chart.series.series-type
+ + @for (type of timeSeriesChartSeriesTypes; track type) { + {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} + } + +
+ } @else { +
{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}
+ } + formControlName="barSettings">
widgets.chart.tooltip-settings
+ formControlName="tooltipValueFormatter" + [globalVariables]="functionScopeVariables" + [functionArgs]="['value', 'latestData']" + functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}" + helpId="widget/lib/flot/tooltip_value_format_fn">
-
-
widgets.time-series-chart.comparison.settings
- - - - - {{ 'widgets.time-series-chart.comparison.show-values-for-comparison' | translate }} - - - - -
-
widgets.time-series-chart.comparison.comparison-values-label
- - - -
-
-
{{ 'widgets.time-series-chart.comparison.comparison-data-color' | translate }}
- - -
-
-
-
+ @if (comparisonEnabled) { +
+
widgets.time-series-chart.comparison.settings
+ + + + + {{ 'widgets.time-series-chart.comparison.show-values-for-comparison' | translate }} + + + + +
+
widgets.time-series-chart.comparison.comparison-values-label
+ + + +
+
+
{{ 'widgets.time-series-chart.comparison.comparison-data-color' | translate }}
+ + +
+
+
+
+ } - -
{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html index 9a562cec25..b607679be2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html @@ -16,56 +16,124 @@ --> -
-
widgets.time-series-chart.series.line.line
-
- - {{ 'widgets.time-series-chart.series.line.show-line' | translate }} - + @if (chartType !== TimeSeriesChartType.point) { +
+
widgets.time-series-chart.series.line.line
+ @if (chartType !== TimeSeriesChartType.state) { +
+ + {{ 'widgets.time-series-chart.series.line.show-line' | translate }} + +
+ } +
+ + {{ 'widgets.time-series-chart.series.line.step-line' | translate }} + + + + @for (stepType of lineSeriesStepTypes; track stepType) { + + {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} + + } + + +
+ @if (chartType !== TimeSeriesChartType.state) { +
+ + {{ 'widgets.time-series-chart.series.line.smooth-line' | translate }} + +
+ } +
+
widgets.time-series-chart.line-type
+ + + @for (lineType of chartLineTypes; track lineType) { + + {{ chartLineTypeTranslations.get(lineType) | translate }} + + } + + +
+
+
widgets.time-series-chart.line-width
+ + + +
-
- - {{ 'widgets.time-series-chart.series.line.step-line' | translate }} + } + @if (chartType !== TimeSeriesChartType.point) { +
+
widgets.time-series-chart.series.point.points
+
+ + {{ 'widgets.time-series-chart.series.point.show-points' | translate }} + +
+ +
+ } @else { +
+ +
+ {{ 'widgets.time-series-chart.series.point.point-label' | translate }} +
- - - - {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} - - - +
+ + + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + } + + + + + + +
-
- - {{ 'widgets.time-series-chart.series.line.smooth-line' | translate }} +
+ + {{ 'widgets.time-series-chart.series.point.point-label-background' | translate }} + +
-
widgets.time-series-chart.line-type
+
widgets.time-series-chart.series.point.point-shape
- - - {{ chartLineTypeTranslations.get(lineType) | translate }} - + + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + }
-
widgets.time-series-chart.line-width
+
widgets.time-series-chart.series.point.point-size
- +
-
-
-
widgets.time-series-chart.series.point.points
-
- - {{ 'widgets.time-series-chart.series.point.show-points' | translate }} - -
- -
+ } @@ -80,9 +148,11 @@
- - {{ chartLabelPositionTranslations.get(position) | translate }} - + @for (position of chartLabelPositions; track position) { + + {{ chartLabelPositionTranslations.get(position) | translate }} + + } widgets.time-series-chart.series.point.point-shape
- - {{ chartShapeTranslations.get(shape) | translate }} - + @for (shape of chartShapes; track shape) { + + {{ chartShapeTranslations.get(shape) | translate }} + + }
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 36bf2aea89..b49d8ad902 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 @@ -56,10 +56,11 @@
- - + + } + formControlName="thresholds" + [aliasController]="aliasController" + [dataKeyCallbacks]="dataKeyCallbacks" + [datasource]="datasource" + [widgetConfig]="widgetConfig?.config" + [yAxisIds]="yAxisIds" + [supportsUnitConversion]="widgetConfig.typeParameters.supportsUnitConversion">
widgets.time-series-chart.chart-style
@@ -86,7 +87,7 @@
+ formControlName="grid">
@@ -98,15 +99,16 @@
widgets.time-series-chart.axis.x-axis
+ formControlName="xAxis" + advanced + axisType="xAxis">
- - + @if (chartType === TimeSeriesChartType.default || chartType === TimeSeriesChartType.bar) { + + + }
@@ -178,9 +180,9 @@
{{ 'tooltip.trigger' | translate }}
{{ 'tooltip.trigger-point' | translate }} + [value]="EChartsTooltipTrigger.point">{{ 'tooltip.trigger-point' | translate }} {{ 'tooltip.trigger-axis' | translate }} + [value]="EChartsTooltipTrigger.axis">{{ 'tooltip.trigger-axis' | translate }}
@@ -210,11 +212,11 @@
+ formControlName="tooltipValueFormatter" + [globalVariables]="functionScopeVariables" + [functionArgs]="['value', 'latestData']" + functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}" + helpId="widget/lib/flot/tooltip_value_format_fn">
@@ -222,7 +224,7 @@
+ formControlName="tooltipDateFormat"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html index 229fb79a62..b6ddd18d6d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html @@ -17,31 +17,36 @@ -->
- - + @if (!fullscreen) { + + } + @if (fullscreen) { + + }
-
- - -
- + @if (!fullscreen) { +
+ + +
+ } @else {
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html index 6d36ee7e24..683b97c588 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html @@ -19,29 +19,31 @@
-
- - - - {{ 'widget.resource-is-extension' | translate }} - - -
+ @for (resource of action.customResources; track resource; let i = $index) { +
+ + + + {{ 'widget.resource-is-extension' | translate }} + + +
+ }
- - - - + @if (hasCustomFunction) { + + + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html index 0e009341ee..e04426ce1b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html @@ -22,40 +22,43 @@
{{ 'widgets.value-action.action' | translate }}
- - {{ getValueActionTranslationsMap.get(action) | translate }} - + @for (action of getValueActions; track action) { + + {{ getValueActionTranslationsMap.get(action) | translate }} + + }
- - + @switch (getValueSettingsFormGroup.get('action').value) { + @case (getValueAction.DO_NOTHING) {
widgets.value-action.value
+ formControlName="defaultValue">
-
- + } + @case (getValueAction.EXECUTE_RPC) {
{{ 'widgets.value-action.method' | translate }}*
- - warning - + @if (getValueSettingsFormGroup.get('executeRpc').get('method').hasError('required') + && getValueSettingsFormGroup.get('executeRpc').get('method').touched) { + + warning + + }
-
- + } + @case (getValueAction.GET_ATTRIBUTE) {
{{ 'widgets.value-action.attribute-scope' | translate }}
@@ -64,9 +67,11 @@ {{ 'attribute.scope-any' | translate }} - - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + }
@@ -85,8 +90,8 @@
- - + } + @case (getValueAction.GET_TIME_SERIES) {
{{ 'widgets.value-action.time-series-key' | translate }}*
@@ -102,15 +107,17 @@
-
- + } + @case (getValueAction.GET_ALARM_STATUS) {
alarm.alarm-severity
- - {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} - + @for (alarmSeverity of alarmSeverities; track alarmSeverity) { + + {{ alarmSeverityTranslationMap.get(alarmSeverity) | translate }} + + }
@@ -124,101 +131,111 @@
-
- -
-
-
widgets.value-action.action-result-converter
- - {{ 'widgets.value-action.converter-none' | translate }} - {{ 'widgets.value-action.converter-function' | translate }} - -
- - -
-
{{ 'widgets.value-action.state-when-result-is' | translate:{state: stateLabel} }}
- +
+
widgets.value-action.action-result-converter
+ + {{ 'widgets.value-action.converter-none' | translate }} + {{ 'widgets.value-action.converter-function' | translate }} + +
+ @if (getValueSettingsFormGroup.get('dataToValue').get('type').value === dataToValueType.FUNCTION) { + + + } + @if (valueType === ValueType.BOOLEAN) { +
+
{{ 'widgets.value-action.state-when-result-is' | translate:{state: stateLabel} }}
+ - + layout="column" + formControlName="compareToValue"> + +
+ }
-
-
- - - - - - widget-config.advanced-settings - - - -
-
{{ 'widgets.value-action.request-timeout-ms' | translate }}
- - - + + + + + + widget-config.advanced-settings + + + +
+
{{ 'widgets.value-action.request-timeout-ms' | translate }}
+ + + @if (getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').touched && + getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid) { + - warning - - -
-
- - - - - {{ 'widgets.value-action.request-persistent' | translate }} - - - - -
-
{{ 'widgets.value-action.persistent-polling-interval' | translate }}
- - - + warning + + } + +
+
+ + + + + {{ 'widgets.value-action.request-persistent' | translate }} + + + + +
+
{{ 'widgets.value-action.persistent-polling-interval' | translate }}
+ + + @if (getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched && + getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid) { + - warning - - -
-
-
-
-
-
-
+ class="tb-error"> + warning +
+ } +
+
+
+
+
+ + +
+ }
- - {{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }} - + @for (actionType of mobileActionTypes; track actionType) { + + {{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }} + + } - - warning - + @if (mobileActionFormGroup.get('type').hasError('required') + && mobileActionFormGroup.get('type').touched) { + + warning + + }
@@ -50,9 +53,11 @@
{{ 'widget-action.mobile.provision-type' | translate }}*
- - {{ provisionTypeTranslationMap.get(type) | translate }} - + @for (type of provisionTypes; track type) { + + {{ provisionTypeTranslationMap.get(type) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts index b836fe2ade..3e6a55ead6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts @@ -76,7 +76,7 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit actionConfig: ActionConfig[]; commonActionConfig: ActionConfig[]; - provisionTypes: string[] = Object.keys(ProvisionType); + provisionTypes = Object.values(ProvisionType); provisionTypeTranslationMap = provisionTypeTranslationMap; private requiredValue: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html index 001d2817ab..3b30491f8c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html @@ -22,39 +22,44 @@
{{ 'widgets.value-action.action' | translate }}
- - {{ setValueActionTranslationsMap.get(action) | translate }} - + @for (action of setValueActions; track action) { + + {{ setValueActionTranslationsMap.get(action) | translate }} + + } - - + @switch (setValueSettingsFormGroup.get('action').value) { + @case (setValueAction.EXECUTE_RPC) {
{{ 'widgets.value-action.method' | translate }}*
- - warning - + @if (setValueSettingsFormGroup.get('executeRpc').get('method').hasError('required') + && setValueSettingsFormGroup.get('executeRpc').get('method').touched) { + + warning + + }
-
- + } + @case (setValueAction.SET_ATTRIBUTE) {
{{ 'widgets.value-action.attribute-scope' | translate }}
- - {{ telemetryTypeTranslationsMap.get(scope) | translate }} - + @for (scope of attributeScopes; track scope) { + + {{ telemetryTypeTranslationsMap.get(scope) | translate }} + + }
@@ -73,8 +78,8 @@
-
- + } + @case (setValueAction.ADD_TIME_SERIES) {
{{ 'widgets.value-action.time-series-key' | translate }}*
@@ -90,99 +95,113 @@
-
-
+ } + }
{{ (setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC ? - 'widgets.value-action.parameters' : 'widgets.value-action.value') | translate }}
+ 'widgets.value-action.parameters' : 'widgets.value-action.value') | translate }}
- {{ 'widgets.value-action.converter-value' | translate }} - {{ 'widgets.value-action.converter-constant' | translate }} + @if (valueType !== ValueType.BOOLEAN) { + {{ 'widgets.value-action.converter-value' | translate }} + } + @if (valueType === ValueType.BOOLEAN) { + {{ 'widgets.value-action.converter-constant' | translate }} + } {{ 'widgets.value-action.converter-function' | translate }} - {{ 'widgets.value-action.converter-none' | translate }} + @if (setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC) { + {{ 'widgets.value-action.converter-none' | translate }} + }
- - - - + formControlName="constantValue"> + + } + @if (setValueSettingsFormGroup.get('valueToData').get('type').value === valueToDataType.FUNCTION) { + + + } -
- - - - - - widget-config.advanced-settings - - - -
-
{{ 'widgets.value-action.request-timeout-ms' | translate }}
- - - + + + + + + widget-config.advanced-settings + + + +
+
{{ 'widgets.value-action.request-timeout-ms' | translate }}
+ + + @if (setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').touched && + setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid) { + - warning - - -
-
- - - - - {{ 'widgets.value-action.request-persistent' | translate }} - - - - -
-
{{ 'widgets.value-action.persistent-polling-interval' | translate }}
- - - + warning + + } + +
+
+ + + + + {{ 'widgets.value-action.request-persistent' | translate }} + + + + +
+
{{ 'widgets.value-action.persistent-polling-interval' | translate }}
+ + + @if (setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched && + setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid) { + - warning - - -
-
-
-
-
-
-
+ class="tb-error"> + warning +
+ } +
+
+
+
+
+
+
+ + }
- - warning - - - - - - - -
- - -
-
{{ 'widget-action.URL' | translate }}
- - - - warning - - -
-
- -
- - {{ 'widget-action.open-right-layout' | translate }} - -
-
- -
- - {{ 'widget-action.open-new-browser-tab' | translate }} - -
-
- -
- - {{ 'widget-action.set-entity-from-widget' | translate }} - -
-
-
{{ 'alias.state-entity-parameter-name' | translate }}
- - - -
-
- -
-
{{ 'widget-action.state-display-type' | translate }}
- - - - {{ stateDisplayTypeName(displayType) }} - - - -
- - -
-
{{ 'widget-action.dialog-title' | translate }}
- - - -
-
- - {{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }} - + } + @case (widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? + widgetActionFormGroup.get('type').value : '') { +
+
+ {{ 'widget-action.target-dashboard-state' | translate }} + @if (widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState) { + * + }
-
-
{{ 'widget-action.dialog-width' | translate }}
- - + + + @if (actionTypeFormGroup.get('targetDashboardStateId').value) { + + } + @if (actionTypeFormGroup.get('targetDashboardStateId').hasError('required') + && actionTypeFormGroup.get('targetDashboardStateId').touched) { warning - -
-
-
{{ 'widget-action.dialog-height' | translate }}
- - + } + + @for (state of filteredDashboardStates | async; track state) { + + + + } + + +
+ } + @case (widgetActionType.openURL) { +
+
{{ 'widget-action.URL' | translate }}
+ + + @if (actionTypeFormGroup.get('url').invalid && actionTypeFormGroup.get('url').touched) { warning - -
- - -
-
{{ 'widget-action.popover-preferred-placement' | translate }}
- - - - {{ popoverPlacementName(placement) }} - - - -
+ } + +
+ } + @case (widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ? + widgetActionFormGroup.get('type').value : '') { +
+ + {{ 'widget-action.open-right-layout' | translate }} + +
+ } + @case (widgetActionFormGroup.get('type').value === widgetActionType.openDashboard || + widgetActionFormGroup.get('type').value === widgetActionType.openURL ? + widgetActionFormGroup.get('type').value : '') { +
+ + {{ 'widget-action.open-new-browser-tab' | translate }} + +
+ } + @case (widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? + widgetActionFormGroup.get('type').value : '') { + @if (widgetType !== WidgetType.static) {
- - {{ 'widget-action.popover-hide-on-click-outside' | translate }} + + {{ 'widget-action.set-entity-from-widget' | translate }}
+ } + @if (actionTypeFormGroup.get('setEntityId').value) {
- - {{ 'widget-action.popover-hide-dashboard-toolbar' | translate }} - -
-
-
{{ 'widget-action.popover-width' | translate }}
- - -
-
-
{{ 'widget-action.popover-height' | translate }}
- - +
{{ 'alias.state-entity-parameter-name' | translate }}
+ + +
- - - - - - - - +
{{ 'widget-action.state-display-type' | translate }}
+ + + @for (displayType of allStateDisplayTypes; track displayType) { + + {{ stateDisplayTypeName(displayType) }} + + } + + +
+ + @switch (actionTypeFormGroup.get('stateDisplayType').value) { + @case ('separateDialog') { +
+
{{ 'widget-action.dialog-title' | translate }}
+ + + +
+
+ + {{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }} + +
+
+
{{ 'widget-action.dialog-width' | translate }}
+ + + @if (stateDisplayTypeFormGroup.get('dialogWidth').invalid && stateDisplayTypeFormGroup.get('dialogWidth').touched) { + + warning + + } + +
+
+
{{ 'widget-action.dialog-height' | translate }}
+ + + @if (stateDisplayTypeFormGroup.get('dialogHeight').invalid && stateDisplayTypeFormGroup.get('dialogHeight').touched) { + + warning + + } + +
+ } + @case ('popover') { +
+
{{ 'widget-action.popover-preferred-placement' | translate }}
+ + + @for (placement of allPopoverPlacements; track placement) { + + {{ popoverPlacementName(placement) }} + + } + + +
+
+ + {{ 'widget-action.popover-hide-on-click-outside' | translate }} + +
+
+ + {{ 'widget-action.popover-hide-dashboard-toolbar' | translate }} + +
+
+
{{ 'widget-action.popover-width' | translate }}
+ + +
+
+
{{ 'widget-action.popover-height' | translate }}
+ + +
+ + } + @case ('normal') { + } + } +
+ } + @case (widgetActionType.custom) { + -
- -
-
{{ 'widget-action.map-item-type' | translate }}
- - - @for(type of mapItemTypes; track type) { - {{ mapItemTypeTranslationMap.get(type) | translate }} - } - - -
- - -
- - - - - - - - + > + } + @case (widgetActionType.placeMapItem) { +
+
{{ 'widget-action.map-item-type' | translate }}
+ + + @for(type of mapItemTypes; track type) { + {{ mapItemTypeTranslationMap.get(type) | translate }} + } + + +
+ + + } + @case (widgetActionFormGroup.get('type').value === widgetActionType.customPretty + || widgetActionFormGroup.get('type').value === widgetActionType.placeMapItem + ? widgetActionFormGroup.get('type').value : '') { + + + } + @case (widgetActionType.mobileAction) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html index a1d2ff2243..03548a16f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html @@ -45,12 +45,14 @@ formControlName="color"> - + @if (!disabled) { + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html index 4acda6d6b4..255003caa4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html @@ -21,7 +21,9 @@ class="mat-block" [appearance]="inlineField ? 'outline' : appearance" [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> - {{ 'entity.entity-alias' | translate }} + @if (showLabel && !inlineField) { + {{ 'entity.entity-alias' | translate }} + }
- - - - warning - + @if (selectEntityAliasFormGroup.get('entityAlias').value) { + + } + @if (selectEntityAliasFormGroup.get('entityAlias').value?.id) { + + } + @if (inlineField && !modelValue && tbRequired + && selectEntityAliasFormGroup.get('entityAlias').touched) { + + warning + + }
- + @if (!inlineField && !selectEntityAliasFormGroup.get('entityAlias').value && !disabled) { + + } - - - - -
-
- entity.no-aliases-found -
- + @for (entityAlias of filteredEntityAliases | async; track entityAlias) { + + + + } + @if (!(filteredEntityAliases | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ entity.no-aliases-found +
+ } @else { + + {{ 'entity.no-alias-matching' | translate : {alias: (searchText | truncate: true: 6: '...')} }} + + } - {{ 'entity.no-alias-matching' | translate : {alias: (searchText | truncate: true: 6: '...')} }} + entity.create-new-alias - - - entity.create-new-alias - -
-
+
+
+ }
- - {{ 'entity.alias-required' | translate }} - + @if (!inlineField && !modelValue && tbRequired) { + + {{ 'entity.alias-required' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html index 16cb7a8e19..9964e6b375 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html @@ -18,17 +18,19 @@
date.time-granularity-formats
-
-
{{ formatTimeUnitTranslations.get(unit) | translate }}
-
- - -
-
- -
{{ previewText[unit] }}
+ @for (unit of formatTimeUnits; track unit) { +
+
{{ formatTimeUnitTranslations.get(unit) | translate }}
+
+ + +
+
+ +
{{ previewText[unit] }}
+
-
+ }
- - - - - - - + @if (limitForm.get('type').value === ValueSourceType.constant) { + + + + } + @if (limitForm.get('type').value === ValueSourceType.latestKey) { + + + } + @if (limitForm.get('type').value === ValueSourceType.entity) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html index d0cffac423..d9a69ad111 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html @@ -22,14 +22,16 @@
widgets.background.background
- - {{ backgroundTypeTranslationsMap.get(type) | translate }} - + @for (type of backgroundTypes; track type) { + + {{ backgroundTypeTranslationsMap.get(type) | translate }} + + }
+ formControlName="imageUrl">
widgets.color.color
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html index 26835e857a..a9cfb92e70 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html @@ -21,18 +21,22 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> - - {{ widgetButtonTypeTranslationMap.get(type) | translate }} - + label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> + @for (type of widgetButtonTypes; track type) { + + {{ widgetButtonTypeTranslationMap.get(type) | translate }} + + } -
- - {{ 'widgets.button.auto-scale' | translate }} - -
+ @if (withAutoScale) { +
+ + {{ 'widgets.button.auto-scale' | translate }} + +
+ }
{{ 'widgets.button.label' | translate }} @@ -63,10 +67,12 @@
-
-
{{ 'widgets.button.border-radius' | translate }}
- -
+ @if (withBorderRadius) { +
+
{{ 'widgets.button.border-radius' | translate }}
+ +
+ }
{{ 'widgets.button.color-palette' | translate }}
@@ -93,16 +99,18 @@ -
-
{{ widgetButtonStateTranslationMap.get(state) | translate }}
- - -
+ @for (state of widgetButtonStates; track state) { +
+
{{ widgetButtonStateTranslationMap.get(state) | translate }}
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.html index 4307c8b90b..6333f0416b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.html @@ -61,19 +61,23 @@
- + @if (copyFromStates?.length) { + + }
- + @for (state of copyFromStates; track state) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html index 4c4544467c..f1bd3f92d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html @@ -27,14 +27,16 @@ [activated]="state === widgetButtonState.activated" [disabled]="state === widgetButtonState.disabled"> - + @if (modelValue) { + + }
+ @if (copyFromStates?.length) { + + }
- + @for (state of copyFromStates; track state) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html index f750ffbd19..ba2c744211 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html @@ -26,14 +26,16 @@ [hovered]="state === widgetButtonToggleState.hovered" [disabled]="state === widgetButtonToggleState.disabled"> - + @if (modelValue) { + + } - - {{ 'widgets.time-series-chart.state.no-states' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html index b603a8ddaa..30ec805710 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html @@ -19,69 +19,81 @@
- - {{ timeSeriesThresholdTypeTranslations.get(type) | translate }} - + @for (type of timeSeriesThresholdTypes; track type) { + + {{ timeSeriesThresholdTypeTranslations.get(type) | translate }} + + } - - + @if (thresholdFormGroup.get('type').value === TimeSeriesChartThresholdType.entity) { + + + }
- - - - warning - - - - - - -
-
- - - - {{ yAxis }} - - - + @if (thresholdFormGroup.get('type').value === TimeSeriesChartThresholdType.constant) { + + + @if (thresholdFormGroup.get('value').hasError('required') + && thresholdFormGroup.get('value').touched) { + + warning + + } + + } + @if (thresholdFormGroup.get('type').value === TimeSeriesChartThresholdType.latestKey) { + + + } + @if (thresholdFormGroup.get('type').value === TimeSeriesChartThresholdType.entity) { + + + }
+ @if (!hideYAxis) { +
+ + + @for (yAxis of yAxisIds; track yAxis) { + + {{ yAxis }} + + } + + +
+ }
+ [widgetConfig]="widgetConfig" + [yAxisIds]="yAxisIds" + [hideYAxis]="hideYAxis" + icon="settings" + [formControl]="thresholdSettingsFormControl"> - +@if (boxButton) { + +} @else { - +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html index 448a31fec0..c2dd3ffab4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html @@ -21,24 +21,34 @@
widgets.time-series-chart.threshold.source
widgets.time-series-chart.threshold.key-value
-
widgets.time-series-chart.axis.y-axis
+ @if (!hideYAxis) { +
widgets.time-series-chart.axis.y-axis
+ }
widgets.color.color
widget-config.units-short
widget-config.decimals-short
-
-
- - - + @if (thresholdsFormArray().controls.length) { +
+ @for (thresholdControl of thresholdsFormArray().controls; track trackByThreshold($index, thresholdControl)) { +
+ + + @if (!$last) { + + } +
+ }
-
+ } @else { + {{ 'widgets.time-series-chart.threshold.no-thresholds' | translate }} + }
- - {{ 'widgets.time-series-chart.threshold.no-thresholds' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html index 0272dd60d2..86c54762c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html @@ -33,27 +33,29 @@ cdkDropList cdkDropListOrientation="vertical" [cdkDropListDisabled]="!dragEnabled" (cdkDropListDropped)="axisDrop($event)"> -
- - -
- + @for (axisControl of axesFormArray().controls; track trackByAxis($index, axisControl)) { +
+ + +
+ +
-
+ }
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 6d6f492619..ceb34f83c7 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 @@ -25,9 +25,11 @@ - - {{ timeSeriesAxisPositionTranslations.get(position) | translate }} - + @for (position of axisPositions; track position) { + + {{ timeSeriesAxisPositionTranslations.get(position) | translate }} + + }
@@ -59,14 +61,16 @@ settings
- + @if (modelValue.id !== 'default') { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html index eb3f84eb0e..58e1a108f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html @@ -19,12 +19,14 @@
widgets.time-series-chart.no-aggregation-bar-width-strategy
- - {{ timeSeriesChartNoAggregationBarWidthStrategyTranslations.get(strategy) | translate }} - + @for (strategy of timeSeriesChartNoAggregationBarWidthStrategies; track strategy) { + + {{ timeSeriesChartNoAggregationBarWidthStrategyTranslations.get(strategy) | translate }} + + }
- + @if (barWidthSettingsFormGroup.get('strategy').value === TimeSeriesChartNoAggregationBarWidthStrategy.group) { - - + } + @if (barWidthSettingsFormGroup.get('strategy').value === TimeSeriesChartNoAggregationBarWidthStrategy.separate) { - + }
@@ -51,8 +53,8 @@ {{ (formGroup.get('relative').value ? - 'widgets.time-series-chart.bar-width-relative' : - 'widgets.time-series-chart.bar-width-absolute') | translate }} + 'widgets.time-series-chart.bar-width-relative' : + 'widgets.time-series-chart.bar-width-absolute') | translate }} {{ 'widgets.time-series-chart.bar-width-relative' | translate }} @@ -62,14 +64,18 @@ - - - % - - - - ms - + @if (formGroup.get('relative').value) { + + + % + + } + @if (!formGroup.get('relative').value) { + + + ms + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html index 83aee39785..d92497ed52 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html @@ -16,94 +16,96 @@ --> -
-
widgets.color.settings-type
- - - {{ 'widgets.color.basic-mode' | translate }} - - - {{ 'widgets.color.advanced-mode' | translate}} - - -
- - + @if (advancedMode) {
-
{{ panelTitle }}
+
widgets.color.settings-type
+ + + {{ 'widgets.color.basic-mode' | translate }} + + + {{ 'widgets.color.advanced-mode' | translate}} + +
-
- -
- -
-
-
-
-
widgets.color.from
- - - + } @else { +
+ @if (panelTitle) { +
{{ panelTitle }}
+ } +
+ } -
widgets.color.to
- - - - - +
+ @switch (colorRangeListFormGroup.get('advancedMode').value) { + @case (false) { +
+ @for (rangeFormGroup of rangeListFormGroups; track trackByRange($index, rangeFormGroup); let isLast = $last) { +
+
+
+
widgets.color.from
+ + + +
widgets.color.to
+ + + + + +
+ +
+
- -
- + }
-
- - - -
-
- - - + } + @case (true) { +
+ @for (advancedRangeControl of advancedRangeControls; track trackByAdvancedRange($index, advancedRangeControl); let isLast = $last) { +
+ + + +
+ }
-
- - - + } + }
- - - -
- -
-
-
- - - + } + + +
+ @for (comp of settingsComponents; track comp) { + + } +
+
+
+ + +
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 f48319af75..2c69cf1fe6 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 @@ -19,10 +19,12 @@
widgets.color.color-settings
- - {{ colorTypeTranslationsMap.get(type) | translate }} - + @for (type of colorTypes; track type) { + + {{ colorTypeTranslationsMap.get(type) | translate }} + + }
@@ -60,19 +62,23 @@
- + @if (settingsComponents?.length) { + + }
- + @for (comp of settingsComponents; track comp) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html index 6306af7c15..101b022685 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html @@ -21,10 +21,11 @@ [disabled]="disabled" #matButton (click)="openColorSettingsPopup($event, matButton)"> - mdi:function-variant + @if (modelValue?.type === colorType.function) { + mdi:function-variant + } @else { +
+
+
+ } - -
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html index 08a0246217..993bea21a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html @@ -21,12 +21,14 @@ breakpoints: { 'lt-md': 1 }}" - label="{{ 'widgets.count.layout' | translate }}" formControlName="layout"> - - {{ countCardLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.count.layout' | translate }}" formControlName="layout"> + @for (layout of countCardLayouts; track layout) { + + {{ countCardLayoutTranslationMap.get(layout) | translate }} + + }
@@ -42,16 +44,16 @@ - }@else { + }@else { } + formControlName="icon"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html index 1881fa7336..008f63544d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html @@ -19,16 +19,17 @@ - - warning - + class="tb-error"> + warning + + } + [width]="flex ? '100%' : ''" formControlName="unit">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html index 3ca5657b2a..61d1d54684 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html @@ -17,8 +17,12 @@ --> - - - {{ cssUnit }} + @if (allowEmpty) { + + + } + @for (cssUnit of cssUnitsList; track cssUnit) { + {{ cssUnit }} + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html index 710d44774c..fbdb34748a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html @@ -17,15 +17,20 @@ --> - {{ dateFormatDisplayValue(dateFormat) }} + @for (dateFormat of dateFormatList; track dateFormat) { + {{ dateFormatDisplayValue(dateFormat) }} + } - - + @if (dateFormatFormControl.value?.custom) { + + } + @if (dateFormatFormControl.value?.auto && !dateFormatFormControl.value?.custom) { + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html index c7ece2becf..84aa13bccb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html @@ -23,52 +23,59 @@ -
-
-
-
{{$index + 1}}
-
- - -
- - -
+ @if (propertiesFormArray().controls.length) { +
+ @for (propertyControl of propertiesFormArray().controls; track trackByProperty($index, propertyControl)) { +
+
+
{{$index + 1}}
+
+ + +
+ @if (!disabled) { + + } + @if (!disabled) { + + } +
+
+ }
-
-
- -
+ } @else { + {{ 'dynamic-form.property.no-items' | translate }} + } + @if (!disabled || !propertiesFormArray().controls.length) { +
+ +
+ }
- - {{ 'dynamic-form.property.no-items' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html index 885fa20ee4..062f300d6b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html @@ -22,72 +22,83 @@
dynamic-form.property.name
dynamic-form.property.type
- - - -
-
-
-
- - -
+ @if (!disabled && importExport) { + + } + @if (!disabled && importExport) { -
+ } + @if (!disabled) { + + }
- -
-
- + @if (propertiesFormArray().controls.length) { +
+ @for (propertyControl of propertiesFormArray().controls; track trackByProperty($index, propertyControl)) { +
+ + +
+ @if (!disabled) { + + } +
+
+ } +
+ } @else { + {{ 'dynamic-form.property.no-properties' | translate }} + } + @if (errorText) { + + }
+ @if (!disabled) { +
+ +
+ }
- - {{ 'dynamic-form.property.no-properties' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html index df90a75dba..00d5c32170 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html @@ -46,42 +46,50 @@
dynamic-form.property.type
- - {{ formPropertyTypeTranslations.get(type) | translate }} - + @for (type of formPropertyTypes; track type) { + + {{ formPropertyTypeTranslations.get(type) | translate }} + + }
-
-
dynamic-form.property.array-item
-
-
dynamic-form.property.item-type
- - - - {{ formPropertyTypeTranslations.get(type) | translate }} - - - -
-
-
dynamic-form.property.item-name
- - - + @if (isArray) { +
+
dynamic-form.property.array-item
+
+
dynamic-form.property.item-type
+ + + @for (type of arrayItemFormPropertyTypes; track type) { + + {{ formPropertyTypeTranslations.get(type) | translate }} + + } + + +
+
+
dynamic-form.property.item-name
+ + + +
+ +
+ } + @if (!isArray) { -
- - - - -
- - {{ 'dynamic-form.property.value-required' | translate }} - -
+ } + @if (![FormPropertyType.fieldset, FormPropertyType.htmlSection, FormPropertyType.array].includes(propertyFormGroup.get('type').value)) { +
+ + {{ 'dynamic-form.property.value-required' | translate }} + +
+ }
@@ -90,7 +98,7 @@ - + @if (isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)) {
dynamic-form.property.sub-label
-
-
-
dynamic-form.property.input-field-suffix
- - - -
-
-
dynamic-form.property.disable-on-property
- - - {{ 'dynamic-form.property.disable-on-property-none' | translate }} - - {{ prop }} - - - -
+ } + @if ([FormPropertyType.number, FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)) { +
+
dynamic-form.property.input-field-suffix
+ + + +
+ } + @if (propertyFormGroup.get('type').value !== FormPropertyType.htmlSection) { +
+
dynamic-form.property.disable-on-property
+ + + {{ 'dynamic-form.property.disable-on-property-none' | translate }} + @for (prop of booleanPropertyIds; track prop) { + + {{ prop }} + + } + + +
+ }
- - - - + @if (isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)) { + + + } + @if (isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value) && FormPropertyType.switch !== propertyFormGroup.get('type').value) { + + + }
@@ -164,154 +182,174 @@ (click)="cancel()"> {{ (disabled ? 'action.close' : 'action.cancel') | translate }} - + @if (!disabled) { + + }
-
-
dynamic-form.property.textarea-rows
- - - -
-
-
dynamic-form.property.number-settings
-
-
dynamic-form.property.min
- - - - -
dynamic-form.property.max
- - - - -
dynamic-form.property.step
- - +
dynamic-form.property.textarea-rows
+ +
-
-
- - - - {{ 'dynamic-form.property.properties' | translate }} - - - - - - - -
-
- - - - {{ 'dynamic-form.property.html-section-settings' | translate }} - - - -
- - -
- - -
-
-
- + } + @if (propertyItemType === FormPropertyType.number) { +
+
dynamic-form.property.number-settings
+
+
dynamic-form.property.min
+ + + + +
dynamic-form.property.max
+ + + + +
dynamic-form.property.step
+ + + +
+
+ } + @if (propertyItemType === FormPropertyType.fieldset) {
- {{ (propertyFormGroup.get('type').value === FormPropertyType.select ? 'dynamic-form.property.select-options' : 'dynamic-form.property.radio-button-options') | translate }} + {{ 'dynamic-form.property.properties' | translate }} -
-
dynamic-form.property.buttons-direction
- - {{ 'dynamic-form.property.direction-column' | translate }} - {{ 'dynamic-form.property.direction-row' | translate }} - -
-
- - {{ 'dynamic-form.property.enable-multiple-select' | translate }} - -
-
- - {{ 'dynamic-form.property.allow-empty-select-option' | translate }} - + + + + +
+ } + @if (propertyItemType === FormPropertyType.htmlSection) { +
+ + + + {{ 'dynamic-form.property.html-section-settings' | translate }} + + + +
+ +
-
-
dynamic-form.property.selected-options-limit
-
-
dynamic-form.property.min
- - - - -
dynamic-form.property.max
- - - + + + + +
+ } + @if ([FormPropertyType.select, FormPropertyType.radios].includes(propertyItemType)) { +
+ + + + {{ (propertyFormGroup.get('type').value === FormPropertyType.select ? 'dynamic-form.property.select-options' : 'dynamic-form.property.radio-button-options') | translate }} + + + + @if (propertyItemType === FormPropertyType.radios) { +
+
dynamic-form.property.buttons-direction
+ + {{ 'dynamic-form.property.direction-column' | translate }} + {{ 'dynamic-form.property.direction-row' | translate }} +
-
+ } + @if (propertyItemType === FormPropertyType.select) { +
+ + {{ 'dynamic-form.property.enable-multiple-select' | translate }} + +
+ } + @if (propertyItemType === FormPropertyType.select && !propertyFormGroup.get('multiple').value) { +
+ + {{ 'dynamic-form.property.allow-empty-select-option' | translate }} + +
+ } + @if (propertyItemType === FormPropertyType.select && propertyFormGroup.get('multiple').value) { +
+
dynamic-form.property.selected-options-limit
+
+
dynamic-form.property.min
+ + + + +
dynamic-form.property.max
+ + + +
+
+ }
- -
-
dynamic-form.property.help-id
- - - -
- + } + @if ([FormPropertyType.javascript, FormPropertyType.markdown].includes(propertyItemType)) { +
+
dynamic-form.property.help-id
+ + + +
+ } + @if (propertyItemType === FormPropertyType.datetime) {
dynamic-form.property.datetime-type
{{ 'dynamic-form.property.datetime-type-date' | translate }} + value="date">{{ 'dynamic-form.property.datetime-type-date' | translate }} {{ 'dynamic-form.property.datetime-type-time' | translate }} + value="time">{{ 'dynamic-form.property.datetime-type-time' | translate }} {{ 'dynamic-form.property.datetime-type-datetime' | translate }} + value="datetime">{{ 'dynamic-form.property.datetime-type-datetime' | translate }}
@@ -319,165 +357,221 @@ {{ 'dynamic-form.property.enable-clear-button' | translate }}
-
-
- - dynamic-form.property.default-value - - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
dynamic-form.property.default-value
- - -
{{ item.label | customTranslate }}
-
-
-
-
- - {{ 'dynamic-form.property.support-unit-conversion' | translate }} - -
-
-
dynamic-form.property.default-value
- - - - - - - - - - - - - - - - - - - - - {{ item.label | customTranslate }} - - - - - {{ item.label | customTranslate }} - - - - - - - - -
+ } + @if (propertyItemType === FormPropertyType.textarea) { +
+ + dynamic-form.property.default-value + + +
+ } + @if (propertyItemType === FormPropertyType.javascript) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.json) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.html) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.css) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.markdown) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.image && !isArray) { +
+ + +
+ } + @if (propertyItemType === FormPropertyType.radios) { +
+
dynamic-form.property.default-value
+ + @for (item of propertyFormGroup.get('items').value; track item) { + +
{{ item.label | customTranslate }}
+
+ } +
+
+ } + @if (propertyItemType === FormPropertyType.units) { +
+ + {{ 'dynamic-form.property.support-unit-conversion' | translate }} + +
+ } + @if (isPropertyTypeAllowedForRow(propertyItemType)) { +
+
dynamic-form.property.default-value
+ @if ([FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)) { + + + + } + @if (propertyItemType === FormPropertyType.color) { + + + } + @if (propertyItemType === FormPropertyType.color_settings) { + + + } + @if (propertyItemType === FormPropertyType.number) { + + + + } + @if (propertyItemType === FormPropertyType.font) { + + + } + @if (propertyItemType === FormPropertyType.units) { + + } + @if (propertyItemType === FormPropertyType.switch) { + + } + @if (propertyItemType === FormPropertyType.icon) { + + + } + @if (propertyItemType === FormPropertyType.select) { + + @if (!propertyFormGroup.get('multiple').value) { + + @if (propertyFormGroup.get('allowEmptyOption').value) { + + } + @for (item of propertyFormGroup.get('items').value; track item) { + + {{ item.label | customTranslate }} + + } + + } + @if (propertyFormGroup.get('multiple').value) { + + @for (item of propertyFormGroup.get('items').value; track item) { + + {{ item.label | customTranslate }} + + } + + } + + } + @if (propertyItemType === FormPropertyType.datetime) { + @if (propertyFormGroup.get('dateTimeType').value === 'date') { + + } + @if (propertyFormGroup.get('dateTimeType').value === 'time') { + + } + @if (propertyFormGroup.get('dateTimeType').value === 'datetime') { + + } + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html index eae448d2a5..1d14969e74 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html @@ -24,9 +24,11 @@ - - {{ formPropertyTypeTranslations.get(type) | translate }} - + @for (type of formPropertyTypes; track type) { + + {{ formPropertyTypeTranslations.get(type) | translate }} + + }
@@ -38,13 +40,15 @@ matTooltipPosition="above"> settings - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html index a15c256745..bcff604829 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html @@ -25,13 +25,15 @@
- + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html index cfb179fe4d..f0096cff5c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html @@ -22,41 +22,49 @@
dynamic-form.property.label
-
-
- - -
- -
+ @if (selectItemsFormArray().controls.length) { +
+ @for (selectItemControl of selectItemsFormArray().controls; track trackBySelectItem($index, selectItemControl)) { +
+ + +
+ @if (!disabled) { + + } +
+
+ }
-
- -
-
- + } @else { + {{ 'dynamic-form.property.no-options' | translate }} + } + @if (errorText) { + + }
+ @if (!disabled) { +
+ +
+ } - - {{ 'dynamic-form.property.no-options' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html index cb1cb71750..c8014dabef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html @@ -18,66 +18,78 @@
-
{{ title }}
+ @if (title) { +
{{ title }}
+ } - - -
- - - -
{{ propertyGroup.title | customTranslate }}
-
-
- - - -
-
- + @for (propertyGroup of propertyGroups; track propertyGroup) { + @if (propertyGroup.visible) { + @if (propertyGroup.title) { +
+ + + +
{{ propertyGroup.title | customTranslate }}
+
+
+ + + +
+
+ } + @if (!propertyGroup.title) { -
-
-
+ } + } + }
- - - - - - - - - - - - - -
- + @if (propertyContainer.visible) { + @if (propertyContainer.type === FormPropertyContainerType.field) { + + + } + @if (propertyContainer.type === FormPropertyContainerType.row) { + + + } + @if (propertyContainer.type === FormPropertyContainerType.fieldset) { + - -
- - -
- -
-
+ title="{{ propertyContainer.label | customTranslate }}" + stroked + [properties]="propertyContainer.properties"> + + } + @if (propertyContainer.type === FormPropertyContainerType.array) { + @if (propertyContainer.arrayItemProperty.type === FormPropertyType.image) { +
+ + +
+ } + @if (propertyContainer.arrayItemProperty.type !== FormPropertyType.image) { + + + } + } + @if (propertyContainer.type === FormPropertyContainerType.htmlSection) { +
+ } + }
-
+ }
@@ -89,185 +101,247 @@
- -
- {{ propertyRow.label | customTranslate }} + @if (propertyRow.switch && propertyRow.switch.visible) { + +
+ {{ propertyRow.label | customTranslate }} +
+
+ } + @if (!propertyRow.switch) { +
+
+ {{ propertyRow.label | customTranslate }} +
- -
-
- {{ propertyRow.label | customTranslate }} + } + @if (propertyRow.properties.length) { +
+ @for (property of propertyRow.properties; track property) { + @if (property.visible) { + @if (property.subLabel) { +
{{ property.subLabel | customTranslate }}
+ } + @if (isInputFieldPropertyType(property.type)) { + + + } + @if (property.type === FormPropertyType.color) { + + + } + @if (property.type === FormPropertyType.color_settings) { + + + } + @if (property.type === FormPropertyType.font) { + + + } + @if (property.type === FormPropertyType.units) { + + } + @if (property.type === FormPropertyType.icon) { + + + } + @if (property.divider) { + + } + } + }
-
-
- - -
{{ property.subLabel | customTranslate }}
- - - - - - - - - - - - - - -
-
-
+ }
- - {{ property.name | customTranslate }} - - - - {{ property.name | customTranslate }} - -
{{ property.fieldSuffix | customTranslate }}
-
- - {{ property.name | customTranslate }} - -
{{ property.fieldSuffix | customTranslate }}
-
- - {{ property.name | customTranslate }} - + @if (singleField) { + {{ property.name | customTranslate }} + } + + + } + @if ([FormPropertyType.text, FormPropertyType.password].includes(property.type)) { + + @if (singleField) { + {{ property.name | customTranslate }} + } + + @if (property.fieldSuffix) { +
{{ property.fieldSuffix | customTranslate }}
+ } +
+ } + @if (property.type === FormPropertyType.number) { + + @if (singleField) { + {{ property.name | customTranslate }} + } + + @if (property.fieldSuffix) { +
{{ property.fieldSuffix | customTranslate }}
+ } +
+ } + @if (property.type === FormPropertyType.select) { + + @if (singleField) { + {{ property.name | customTranslate }} + } + + @if (!property.multiple && property.allowEmptyOption) { + + } + @for (item of property.items; track item) { + + {{ item.label | customTranslate }} + + } + + + } + @if (property.type === FormPropertyType.datetime) { + + + } + @if (property.type === FormPropertyType.javascript) { +
+ + +
+ } + @if (property.type === FormPropertyType.json) { +
+ + +
+ } + @if (property.type === FormPropertyType.html) { +
+ - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
{{ property.name | customTranslate }}
- - -
{{ item.label | customTranslate }}
-
-
-
+
+
+ } + @if (property.type === FormPropertyType.css) { +
+ + +
+ } + @if (property.type === FormPropertyType.markdown) { +
+ + +
+ } + @if (property.type === FormPropertyType.image) { +
+ + +
+ } + @if (property.type === FormPropertyType.radios) { +
+
{{ property.name | customTranslate }}
+ + @for (item of property.items; track item) { + +
{{ item.label | customTranslate }}
+
+ } +
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.html index 329a6aaf97..c11e528911 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.html @@ -24,26 +24,31 @@ [formControl]="entityAliasFormControl" (focusin)="onEntityAliasFocus()" [matAutocomplete]="entityAliasAutocomplete"> - - warning - - + @if (required && entityAliasFormControl.hasError('required') + && entityAliasFormControl.touched) { + + warning + + } + @if (entityAliasFormControl.value && !entityAliasFormControl.disabled) { + + } - - - + @for (entityAlias of filteredEntityAliases | async; track entityAlias) { + + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html index 9ca50c55f0..326832d34e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html @@ -21,7 +21,9 @@ class="mat-block" [appearance]="inlineField ? 'outline' : appearance" [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> - {{ 'filter.filter' | translate }} + @if (showLabel && !inlineField) { + {{ 'filter.filter' | translate }} + }
- - - warning - + @if (selectFilterFormGroup.get('filter').value && !disabled) { + + } + @if (inlineField && !modelValue && tbRequired + && selectFilterFormGroup.get('filter').touched) { + + warning + + }
- + @if (!inlineField && !selectFilterFormGroup.get('filter').value && !disabled) { + + } - - - - -
-
- filter.no-filters-found -
- + @for (filter of filteredFilters | async; track filter) { + + + + } + @if (!(filteredFilters | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ filter.no-filters-found +
+ } @else { + + {{ 'filter.no-filter-matching' | translate : {filter: (searchText | truncate: true: 6: '...')} }} + + } - {{ 'filter.no-filter-matching' | translate : {filter: (searchText | truncate: true: 6: '...')} }} + filter.create-new-filter - - - filter.create-new-filter - -
-
+
+
+ }
- - {{ 'filter.filter-required' | translate }} - + @if (!inlineField && !modelValue && tbRequired) { + + {{ 'filter.filter-required' | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html index f0bf2aa5cf..b795dfe25b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html @@ -19,13 +19,17 @@
widgets.widget-font.font-settings
widgets.widget-font.size
-
- - - - -
-
widgets.widget-font.auto
+ @if (!autoScale) { +
+ + + + +
+ } + @if (autoScale) { +
widgets.widget-font.auto
+ }
widgets.widget-font.font-family
@@ -33,19 +37,23 @@ - + @if (fontFormGroup.get('family').value) { + + } - - - + @for (family of filteredFontFamilies | async; track family) { + + + + }
@@ -55,9 +63,11 @@ - - {{ fontWeightTranslationsMap.has(weight) ? (fontWeightTranslationsMap.get(weight) | translate) : weight }} - + @for (weight of fontWeightsList; track weight) { + + {{ fontWeightTranslationsMap.has(weight) ? (fontWeightTranslationsMap.get(weight) | translate) : weight }} + + }
@@ -67,33 +77,45 @@ - - {{ fontStyleTranslationsMap.get(style) | translate }} - + @for (style of fontStylesList; track style) { + + {{ fontStyleTranslationsMap.get(style) | translate }} + + }
-
-
widgets.widget-font.line-height
- - - -
widgets.widget-font.auto
-
+ @if (!disabledLineHeight) { +
+
widgets.widget-font.line-height
+ @if (!autoScale) { + + + + } + @if (autoScale) { +
widgets.widget-font.auto
+ } +
+ }
widgets.widget-font.preview
- - + @if (clearButton) { + + } + @if (clearButton) { + + } + +
+ + } + + + } + +
+
+
widgets.color.gradient-end
+
+
+
widgets.color.end-value
+ + + +
+ + formControlName="end"> -
- - +
- - - -
-
-
widgets.color.gradient-end
-
-
-
widgets.color.end-value
- - - + } + @case (true) { +
+
+
+ widgets.color.gradient-start + widgets.color.gradient-start-min
- +
+ + + formControlName="color"> - -
-
-
- - -
-
-
- widgets.color.gradient-start - widgets.color.gradient-start-min -
-
- - - - +
-
- -
-
-
-
-
-
- widgets.color.gradient-color - widgets.color.gradient-color-min -
-
- - - - + @if (advancedGradientListFormGroups.length) { +
+
+ @for (gradientFormGroup of advancedGradientListFormGroups; track trackByGradient($index, gradientFormGroup); let isLast = $last) { +
+
+
+
+ widgets.color.gradient-color + widgets.color.gradient-color-min +
+
+ + + + +
+
+ + +
-
- - + }
-
-
- - -
-
-
- widgets.color.gradient-end - widgets.color.gradient-end-min -
-
- - - - + } + +
+
+
+ widgets.color.gradient-end + widgets.color.gradient-end-min +
+
+ + + + +
-
- + } + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html index b8cbb8ba82..93a1ed6adc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html @@ -28,15 +28,17 @@ - -
-
-
{{ option.name }}
-
- + @for (option of options; track option) { + +
+
+
{{ option.name }}
+
+ +
-
- + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html index 485f6d0af4..f89fde663a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html @@ -16,39 +16,43 @@ --> -
- - {{ 'widgets.status-widget.label' | translate }} - -
- - - + @if (layout !== StatusWidgetLayout.icon) { +
+ + {{ 'widgets.status-widget.label' | translate }} + +
+ + + +
-
-
- - {{ 'widgets.status-widget.status' | translate }} - -
- - - - - + } + @if (layout !== StatusWidgetLayout.icon) { +
+ + {{ 'widgets.status-widget.status' | translate }} + +
+ + + + + +
-
+ }
{{ 'widgets.status-widget.icon' | translate }} @@ -74,13 +78,17 @@ formControlName="primaryColor">
- -
-
widgets.status-widget.secondary
- - -
+ @if (layout !== StatusWidgetLayout.icon) { + + } + @if (layout !== StatusWidgetLayout.icon) { +
+
widgets.status-widget.secondary
+ + +
+ }
widgets.status-widget.background
@@ -100,13 +108,17 @@ formControlName="primaryColorDisabled">
- -
-
widgets.status-widget.secondary
- - -
+ @if (layout !== StatusWidgetLayout.icon) { + + } + @if (layout !== StatusWidgetLayout.icon) { +
+
widgets.status-widget.secondary
+ + +
+ }
widgets.status-widget.background
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html index 5b91e07224..bee0b3ab1c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html @@ -19,40 +19,46 @@

{{ 'datakey.configuration' | translate }}

- - + @if (hasAdvanced) { + + + }
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
+ [dataKeyConfigMode]="dataKeyConfigMode" + [dataKeySettingsForm]="data.dataKeySettingsForm" + [dataKeySettingsDirective]="data.dataKeySettingsDirective" + [deviceId]="data.deviceId" + [entityAliasId]="data.entityAliasId" + [dashboard]="data.dashboard" + [aliasController]="data.aliasController" + [widget]="data.widget" + [widgetType]="data.widgetType" + [showPostProcessing]="data.showPostProcessing" + [callbacks]="data.callbacks" + [hideDataKeyName]="data.hideDataKeyName" + [hideDataKeyLabel]="data.hideDataKeyLabel" + [hideDataKeyColor]="data.hideDataKeyColor" + [hideDataKeyUnits]="data.hideDataKeyUnits" + [hideDataKeyDecimals]="data.hideDataKeyDecimals" + [hideDataKeyAggregation]="data.hideDataKeyAggregation" + [supportsUnitConversion]="data.supportsUnitConversion" + formControlName="dataKey">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html index 85acdab5cf..d5aa8967c7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html @@ -15,185 +15,218 @@ limitations under the License. --> -
-
-
-
datakey.general
- - {{ 'entity.key' | translate }} - - - - - - - - - - datakey.label - - - -
-
widget-config.units-short
- - -
-
-
widget-config.decimals-short
- - - -
-
-
-
{{ 'datakey.color' | translate }}
- - -
-
- datakey.data-generation-func -
- - -
- -
-
-
{{ 'datakey.aggregation' | translate }}
- - - - {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} + close + + } + + @for (key of filteredKeys | async; track key) { + + - - -
-
- {{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }} -
- {{ 'datakey.aggregation-type-hint-common' | translate }} -
-
-
-
-
-
-
datakey.delta-calculation
-
- - - - - {{ 'datakey.enable-delta-calculation' | translate }} - - {{ 'datakey.enable-delta-calculation-hint' | translate }} - - - - + } + + + } + @if (!hideDataKeyLabel) { + + datakey.label + + + } + @if (modelValue.type !== dataKeyTypes.alarm) { + @if (!hideDataKeyUnits) {
-
{{ 'widgets.chart.time-for-comparison' | translate }}
- - - - {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - - {{ 'widgets.chart.time-for-comparison-days' | translate }} - - - {{ 'widgets.chart.time-for-comparison-weeks' | translate }} - - - {{ 'widgets.chart.time-for-comparison-months' | translate }} - - - {{ 'widgets.chart.time-for-comparison-years' | translate }} - - - {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} - - - +
widget-config.units-short
+ +
-
-
{{ 'widgets.chart.custom-interval-value' | translate }}
- - + } + @if (!hideDataKeyDecimals) { +
+
widget-config.decimals-short
+ +
-
-
{{ 'datakey.delta-calculation-result' | translate }}
+ } + } + @if (!hideDataKeyColor) { +
+
{{ 'datakey.color' | translate }}
+ + +
+ } + @if (modelValue.type === dataKeyTypes.function) { +
+ datakey.data-generation-func +
+ + +
+ } + @if (!hideDataKeyAggregation && widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries) { +
+
+
{{ 'datakey.aggregation' | translate }}
- - - {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} - + + @for (aggregation of aggregations; track aggregation) { + + {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} + + }
- - - +
+ {{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }} + @if (dataKeyFormGroup.get('aggregationType').value !== aggregationTypes.NONE) { +
+ {{ 'datakey.aggregation-type-hint-common' | translate }} +
+ } +
+
+ }
-
-
- - - - - {{ 'datakey.use-data-post-processing-func' | translate }} - - - - - - - - - + @if (dataKeyFormGroup.get('aggregationType').value && dataKeyFormGroup.get('aggregationType').value !== aggregationTypes.NONE) { +
+
datakey.delta-calculation
+
+ + + + + {{ 'datakey.enable-delta-calculation' | translate }} + + {{ 'datakey.enable-delta-calculation-hint' | translate }} + + + + @if (dataKeyFormGroup.get('comparisonEnabled').value) { +
+
{{ 'widgets.chart.time-for-comparison' | translate }}
+ + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + +
+ @if (dataKeyFormGroup.get('timeForComparison').value === 'customInterval') { +
+
{{ 'widgets.chart.custom-interval-value' | translate }}
+ + + +
+ } +
+
{{ 'datakey.delta-calculation-result' | translate }}
+ + + @for (comparisonResultType of comparisonResults; track comparisonResultType) { + + {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} + + } + + +
+ } +
+
+
+
+ } + @if ((modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute || modelValue.type === dataKeyTypes.count) && showPostProcessing) { +
+ + + + + {{ 'datakey.use-data-post-processing-func' | translate }} + + + + + @if (dataKeyFormGroup.get('usePostProcessing').value) { + + + } + @if (dataKeyFormGroup.get('usePostProcessing').value) { + + } + + +
+ }
-
- +} @else {
-
+} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html index 545c944ec7..f1e494ff21 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html @@ -16,26 +16,32 @@ --> -
-
{{ 'legend.direction' | translate }}
- - - - {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} - - - -
+ @if (!hideDirection) { +
+
{{ 'legend.direction' | translate }}
+ + + @for (direction of legendDirections; track direction) { + + {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} + + } + + +
+ }
{{ 'legend.position' | translate }}
- - {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} - + {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html index b79c6ec5a0..7989ee86e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html @@ -19,34 +19,39 @@
- - {{ datasourceTypesTranslations.get(type) | translate }} - + @for (type of datasourceTypes; track type) { + + {{ datasourceTypesTranslations.get(type) | translate }} + + } - - - - - - - + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.function) { + + + + } + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.device) { + + + } + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.entity) { + + + }
widgets.maps.data-layer.data-keys
-
-
- - + @if (dataSourcesFormArray().controls.length) { +
+ @for (dataSourceControl of dataSourcesFormArray().controls; track trackByDataSource($index, dataSourceControl)) { +
+ + +
+ }
-
+ } @else { + {{ 'widgets.maps.data-layer.no-datasources' | translate }} + }
- - {{ 'widgets.maps.data-layer.no-datasources' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html index 360461b1ce..b4a8555f97 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.html @@ -21,10 +21,11 @@ [disabled]="disabled" #matButton (click)="openColorSettingsPopup($event, matButton)"> - mdi:function-variant + @if (modelValue?.type === DataLayerColorType.function) { + mdi:function-variant + } @else { +
+
+
+ } - -
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html index 4429957983..3941753d57 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html @@ -33,30 +33,36 @@ - - - - - + @if (patternSettingsFormGroup.get('type').value === DataLayerPatternType.pattern) { + + + } + @if (patternSettingsFormGroup.get('type').value === DataLayerPatternType.function) { + + + } + @if (patternType === 'tooltip') {
widgets.maps.data-layer.tooltip-trigger
- - {{ dataLayerTooltipTriggerTranslationMap.get(trigger) | translate }} - + @for (trigger of dataLayerTooltipTriggers; track trigger) { + + {{ dataLayerTooltipTriggerTranslationMap.get(trigger) | translate }} + + }
@@ -65,30 +71,32 @@ {{ 'widgets.maps.data-layer.auto-close-tooltips' | translate }}
-
-
widgets.maps.data-layer.tooltip-offset
-
-
-
widgets.maps.data-layer.tooltip-offset-horizontal
- - - -
-
-
widgets.maps.data-layer.tooltip-offset-vertical
- - - + @if (hasTooltipOffset) { +
+
widgets.maps.data-layer.tooltip-offset
+
+
+
widgets.maps.data-layer.tooltip-offset-horizontal
+ + + +
+
+
widgets.maps.data-layer.tooltip-offset-vertical
+ + + +
-
+ } - + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html index 0d83a4e423..9d9f4377e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/image-map-source-settings.component.html @@ -25,11 +25,13 @@ {{ 'widgets.maps.image.image-source-entity-key' | translate }}
- - - + @if (ImageSourceType.image === imageMapSourceFormGroup.get('sourceType').value) { + + + } + @if (ImageSourceType.entityKey === imageMapSourceFormGroup.get('sourceType').value) {
widgets.maps.image.source-entity-alias
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html index 421d518a90..6cab6208f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html @@ -18,14 +18,15 @@
- - warning - + @if (mapActionButton.hasError('invalidButtonConfig')) { + + warning + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 886c9fb681..8e484c8e3d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -30,159 +30,172 @@
{{ 'widget-config.datasource' | translate }}
- {{ datasourceTypesTranslations.get(type) | translate }} + @for (type of datasourceTypes; track type) { + {{ datasourceTypesTranslations.get(type) | translate }} + }
- - datasource.label - - - - - - - - + @if (dataLayerFormGroup.get('dsType').value === DatasourceType.function) { + + datasource.label + + + } + @if (dataLayerFormGroup.get('dsType').value === DatasourceType.device) { + + + } + @if (dataLayerFormGroup.get('dsType').value === DatasourceType.entity) { + + + } + @if ([DatasourceType.device, DatasourceType.entity].includes(dataLayerFormGroup.get('dsType').value)) { + + + }
- - - - widgets.maps.data-layer.more-datasources - - - - - - - + @if (dataLayerFormGroup.get('dsType').value !== DatasourceType.function) { + + + + widgets.maps.data-layer.more-datasources + + + + + + + + }
{{ 'datakey.keys' | translate }}
-
+ @if (['trips', 'markers'].includes(dataLayerType)) { +
+ + + + +
+ } + @if (dataLayerType === 'polygons') { + (keyEdit)="editKey('polygonKey')" + formControlName="polygonKey"> + } + @if (dataLayerType === 'circles') { + (keyEdit)="editKey('circleKey')" + formControlName="circleKey"> -
- - - - - - + } + @if (dataLayerType === 'polylines') { + + + }
{{ 'widget-config.appearance' | translate }}
- + @if (['trips', 'markers'].includes(dataLayerType)) {
@@ -230,28 +243,34 @@ -
-
widgets.maps.data-layer.marker.shape
- -
-
-
widgets.maps.data-layer.marker.icon
- -
-
-
widgets.maps.data-layer.marker.image
- -
+ @if (dataLayerFormGroup.get('markerType').value === MarkerType.shape) { +
+
widgets.maps.data-layer.marker.shape
+ +
+ } + @if (dataLayerFormGroup.get('markerType').value === MarkerType.icon) { +
+
widgets.maps.data-layer.marker.icon
+ +
+ } + @if (dataLayerFormGroup.get('markerType').value === MarkerType.image) { +
+
widgets.maps.data-layer.marker.image
+ +
+ }
widgets.maps.data-layer.marker.marker-offset
@@ -267,18 +286,20 @@
-
- - {{ 'widgets.maps.data-layer.marker.rotate-marker' | translate }} - -
-
widgets.maps.data-layer.marker.offset-angle
- - -
deg
-
+ @if (dataLayerType === 'trips') { +
+ + {{ 'widgets.maps.data-layer.marker.rotate-marker' | translate }} + +
+
widgets.maps.data-layer.marker.offset-angle
+ + +
deg
+
+
-
+ } @if (dataLayerType === 'trips') { @@ -286,19 +307,21 @@
-
-
widgets.maps.data-layer.marker.position-conversion
- - -
- - + @if (mapType === MapType.image) { +
+
widgets.maps.data-layer.marker.position-conversion
+ + +
+ } + } + @if (dataLayerType === 'trips') {
@@ -322,7 +345,7 @@ [dsType]="dataLayerFormGroup.get('dsType').value" [dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value" - helpId="widget/lib/map/path_color_fn" formControlName="pathStrokeColor"> + helpId="widget/lib/map/path_color_fn" formControlName="pathStrokeColor">
@@ -341,9 +364,11 @@
- - {{ pathDecoratorSymbolTranslationMap.get(symbol) | translate }} - + @for (symbol of pathDecoratorSymbols; track symbol) { + + {{ pathDecoratorSymbolTranslationMap.get(symbol) | translate }} + + }
@@ -413,7 +438,7 @@ [dsType]="dataLayerFormGroup.get('dsType').value" [dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value" - helpId="widget/lib/map/path_point_color_fn" formControlName="pointColor"> + helpId="widget/lib/map/path_point_color_fn" formControlName="pointColor">
-
- + } + @if (['polygons', 'circles'].includes(dataLayerType)) {
{{ 'widgets.maps.data-layer.shape.fill' | translate }}
@@ -438,33 +463,39 @@ {{ 'widgets.maps.data-layer.shape.fill-type-image' | translate }}
-
-
widgets.maps.data-layer.shape.color
- +
widgets.maps.data-layer.shape.color
+ -
-
-
widgets.maps.data-layer.shape.stripe-pattern
- - -
-
-
widgets.maps.data-layer.shape.image
- -
+
+ } + @if (dataLayerFormGroup.get('fillType').value === ShapeFillType.stripe) { +
+
widgets.maps.data-layer.shape.stripe-pattern
+ + +
+ } + @if (dataLayerFormGroup.get('fillType').value === ShapeFillType.image) { +
+
widgets.maps.data-layer.shape.image
+ +
+ }
- - + } + @if (['polygons', 'circles', 'polylines'].includes(dataLayerType)) {
widgets.maps.data-layer.shape.stroke
@@ -479,10 +510,10 @@ [dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value" helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : (dataLayerType === 'circles' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/polyline_stroke_color_fn') }}" - formControlName="strokeColor"> + formControlName="strokeColor">
-
+ } @if (dataLayerType !== 'trips') { } @@ -500,39 +531,46 @@ formControlName="groups">
-
-
{{ dataLayerEditTitle | translate }}
-
-
widgets.maps.data-layer.edit-instruments
- - - {{ dataLayerEditActionTranslationMap.get(action) | translate }} - - -
-
-
widgets.maps.data-layer.persist-location-attribute-scope
- - - {{ telemetryTypeTranslationsShort.get(AttributeScope.SERVER_SCOPE) | translate }} - - - {{ telemetryTypeTranslationsShort.get(AttributeScope.SHARED_SCOPE) | translate }} - - -
-
- - - {{ 'widgets.maps.data-layer.enable-snapping' | translate }} - - + @if (dataLayerType !== 'trips') { +
+
{{ dataLayerEditTitle | translate }}
+
+
widgets.maps.data-layer.edit-instruments
+ + @for (action of dataLayerEditActions; track action) { + + {{ dataLayerEditActionTranslationMap.get(action) | translate }} + + } + +
+ @if (showEditAttributeScope) { +
+
widgets.maps.data-layer.persist-location-attribute-scope
+ + + {{ telemetryTypeTranslationsShort.get(AttributeScope.SERVER_SCOPE) | translate }} + + + {{ telemetryTypeTranslationsShort.get(AttributeScope.SHARED_SCOPE) | translate }} + + +
+ } +
+ + + {{ 'widgets.maps.data-layer.enable-snapping' | translate }} + + +
-
- - + } + @if (dataLayerType === 'markers') { + + + }
+ @if (['trips', 'markers'].includes(dataLayerType)) { + - - - -
- - - - - - - - - - + [widgetType]="dataLayerType === 'trips' ? widgetType.timeseries : widgetType.latest" + [dataKeyType]="context.functionsOnly ? DataKeyType.function : null" + [dataKeyTypes]="dataLayerType === 'trips' ? [DataKeyType.timeseries] : [DataKeyType.attribute, DataKeyType.timeseries]" + [callbacks]="context.callbacks" + [generateKey]="context.generateDataKey" + (keyEdit)="editKey('xKey')" + formControlName="xKey"> + + } + @if (['trips', 'markers'].includes(dataLayerType)) { + + + } + @if (dataLayerType === 'polygons') { + + + } + @if (dataLayerType === 'circles') { + + + } + @if (dataLayerType === 'polylines') { + + + }
-
+ @if (dataLayersFormArray().controls.length) { +
+ @for (dataLayerControl of dataLayersFormArray().controls; track trackByDataLayer($index, dataLayerControl)) { +
+ + +
+ +
+
+ }
-
+ } @else { + {{ noDataLayersText | translate }} + }
- - {{ noDataLayersText | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.html index fcec47bc2b..2a2b6c27a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.html @@ -19,29 +19,33 @@
- - {{ datasourceTypesTranslations.get(type) | translate }} - + @for (type of datasourceTypes; track type) { + + {{ datasourceTypesTranslations.get(type) | translate }} + + } - - - - + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.device) { + + + } + @if (dataSourceFormGroup.get('dsType').value === DatasourceType.entity) { + + + }
widgets.maps.data-layer.filter
-
-
- - + @if (dataSourcesFormArray().controls.length) { +
+ @for (dataSourceControl of dataSourcesFormArray().controls; track trackByDataSource($index, dataSourceControl)) { +
+ + +
+ }
-
+ } @else { + {{ 'widgets.maps.data-layer.no-datasources' | translate }} + }
- - {{ 'widgets.maps.data-layer.no-datasources' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html index d59fe2d283..4416ab2d6c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html @@ -21,47 +21,67 @@ - - {{ mapProviderTranslationMap.get(provider) | translate }} - + @for (provider of mapProviders; track provider) { + + {{ mapProviderTranslationMap.get(provider) | translate }} + + } - - - - {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ googleMapLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ hereLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ tencentLayerTranslationMap.get(layerType) | translate }} - - - - - - + @if (MapProvider.openstreet === layerFormGroup.get('provider').value) { + + + @for (layerType of openStreetLayerTypes; track layerType) { + + {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.google === layerFormGroup.get('provider').value) { + + + @for (layerType of googleMapLayerTypes; track layerType) { + + {{ googleMapLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.here === layerFormGroup.get('provider').value) { + + + @for (layerType of hereLayerTypes; track layerType) { + + {{ hereLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.tencent === layerFormGroup.get('provider').value) { + + + @for (layerType of tencentLayerTypes; track layerType) { + + {{ tencentLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.custom === layerFormGroup.get('provider').value) { + + + + }
- - {{ mapProviderTranslationMap.get(provider) | translate }} - + @for (provider of mapProviders; track provider) { + + {{ mapProviderTranslationMap.get(provider) | translate }} + + }
{{ (MapProvider.custom === layerFormGroup.get('provider').value ? 'widgets.maps.layer.provider.custom.tile-url' : 'widgets.maps.layer.layer') | translate }}
- - - - {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ googleMapLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ hereLayerTranslationMap.get(layerType) | translate }} - - - - - - - {{ tencentLayerTranslationMap.get(layerType) | translate }} - - - - - - -
-
-
widgets.maps.layer.credentials.credentials
-
-
widgets.maps.layer.credentials.api-key
- - + @if (MapProvider.openstreet === layerFormGroup.get('provider').value) { + + + @for (layerType of openStreetLayerTypes; track layerType) { + + {{ openStreetMapLayerTranslationMap.get(layerType) | translate }} + + } + -
+ } + @if (MapProvider.google === layerFormGroup.get('provider').value) { + + + @for (layerType of googleMapLayerTypes; track layerType) { + + {{ googleMapLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.here === layerFormGroup.get('provider').value) { + + + @for (layerType of hereLayerTypes; track layerType) { + + {{ hereLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.tencent === layerFormGroup.get('provider').value) { + + + @for (layerType of tencentLayerTypes; track layerType) { + + {{ tencentLayerTranslationMap.get(layerType) | translate }} + + } + + + } + @if (MapProvider.custom === layerFormGroup.get('provider').value) { + + + + }
+ @if ([MapProvider.google, MapProvider.here].includes(layerFormGroup.get('provider').value)) { +
+
widgets.maps.layer.credentials.credentials
+
+
widgets.maps.layer.credentials.api-key
+ + + +
+
+ }
widgets.maps.layer.reference.reference-layer
{{ 'widgets.maps.layer.reference.no-layer' | translate }} - - {{ referenceLayerTypeTranslationMap.get(layer) | translate }} - + @for (layer of referenceLayerTypes; track layer) { + + {{ referenceLayerTypeTranslationMap.get(layer) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.html index 9f4072fc12..9c824c71c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layers.component.html @@ -23,28 +23,34 @@
widgets.maps.layer.layer
-
-
- -
- -
+ @if (layersFormArray().controls.length) { +
+ @for (layerControl of layersFormArray().controls; track trackByLayer($index, layerControl)) { +
+ +
+ +
+
+ }
-
+ } @else { + {{ 'widgets.maps.layer.no-layers' | translate }} + }
- - {{ 'widgets.maps.layer.no-layers' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html index 083178ddb3..943d166a0b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html @@ -26,13 +26,17 @@ {{ 'widgets.maps.map-type.image' | translate }}
- - - - + @if (MapType.geoMap === mapSettingsFormGroup.get('mapType').value) { + + + } + @if (MapType.image === mapSettingsFormGroup.get('mapType').value) { + + + }
@@ -41,46 +45,50 @@
- {{ 'widgets.maps.overlays.trips' | translate }} + @if (trip) { + {{ 'widgets.maps.overlays.trips' | translate }} + } {{ 'widgets.maps.overlays.markers' | translate }} {{ 'widgets.maps.overlays.polygons' | translate }} {{ 'widgets.maps.overlays.circles' | translate }} {{ 'widgets.maps.overlays.polylines' | translate }}
- + @if (trip) { + + } + [mapType]="mapSettingsFormGroup.get('mapType').value"> + [mapType]="mapSettingsFormGroup.get('mapType').value"> + [mapType]="mapSettingsFormGroup.get('mapType').value"> + [mapType]="mapSettingsFormGroup.get('mapType').value">
{{ 'widgets.maps.data-layer.additional-datasources' | translate }}
+ [context]="context">
@@ -91,35 +99,45 @@
widgets.maps.control.position
- - {{ mapControlsPositionTranslationMap.get(position) | translate }} - + @for (position of mapControlPositions; track position) { + + {{ mapControlsPositionTranslationMap.get(position) | translate }} + + }
widgets.maps.control.zoom-actions
- - {{ mapZoomActionTranslationMap.get(action) | translate }} - + @for (action of mapZoomActions; track action) { + + {{ mapZoomActionTranslationMap.get(action) | translate }} + + }
widgets.maps.control.scale
- - {{ mapScaleTranslationMap.get(scale) | translate }} - + @for (scale of mapScales; track scale) { + + {{ mapScaleTranslationMap.get(scale) | translate }} + + }
-
- - {{ 'widgets.maps.control.switch-to-drag-mode-using-button' | translate }} - -
- + @if (showDragButtonModeButtonSettings) { +
+ + {{ 'widgets.maps.control.switch-to-drag-mode-using-button' | translate }} + +
+ } + @if (trip) { + + } @@ -127,7 +145,7 @@
{{ 'widgets.maps.common.common-map-settings' | translate }}
- + @if (MapType.geoMap === mapSettingsFormGroup.get('mapType').value) {
{{ 'widgets.maps.common.fit-map-bounds' | translate }} @@ -147,7 +165,7 @@
-
+ }
widgets.maps.common.entities-limit
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html index 1ca905dbe7..2c3d443378 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component.html @@ -19,49 +19,57 @@
widgets.maps.data-layer.tooltip-tag-actions
- -
-
-
- - {{ action.name }} - + @for (action of actionsFormGroup.get('actions').value; track action) { + +
+
+
+ + {{ action.name }} + +
+ @if (!disabled) { + + } + @if (!disabled) { + + }
- - -
- + + } - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html index 150789ffbd..c6b8d4eefd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-icon-shapes.component.html @@ -18,10 +18,10 @@
widgets.maps.data-layer.marker.marker-icon
+ [popover]="popover" (iconSelected)="selectIcon($event)">
widgets.maps.data-layer.marker.marker-appearance
- + @for (containerInfo of iconContainers; track containerInfo) { @if (containerInfo.iconContainer === iconContainer) { } - + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html index 22d7a09f48..2a3d3fd812 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.html @@ -21,8 +21,9 @@ [disabled]="disabled" #matButton (click)="openImageSettingsPopup($event, matButton)"> - mdi:function-variant + @if (modelValue?.type === MarkerImageType.function) { + mdi:function-variant + } @else { + + } - - - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html index 0c76ad3fe5..5961577162 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.html @@ -26,13 +26,17 @@ [disabled]="disabled" #matButton (click)="openShapePopup($event, matButton)"> - -
-
+ @if (markerType === MarkerType.shape) { + + } + @if (markerType === MarkerType.icon) { +
+
+ } + helpId="widget/lib/map/color_fn" formControlName="color">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html index e9611bf955..2e94accae9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shapes.component.html @@ -18,21 +18,25 @@
widgets.maps.data-layer.marker.marker-shapes
- - - - + @for (shapeInfo of shapes; track shapeInfo) { + @if (shapeInfo.shape === shape) { + + } + @if (shapeInfo.shape !== shape) { + + } + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html index f890575008..e5157f09ea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html @@ -21,9 +21,10 @@ [disabled]="disabled" #matButton (click)="openImageSettingsPopup($event, matButton)"> - mdi:function-variant + @if (modelValue?.type === ShapeFillImageType.function) { + mdi:function-variant + } @else { + + } - - - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html index 966cef8c3f..36b6ed86fb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/trip-timeline-settings.component.html @@ -38,9 +38,11 @@
widgets.maps.timeline.speed-options
- - {{ speed }} - + @for (speed of [1,5,10,15,25]; track speed) { + + {{ speed }} + + }
@@ -50,7 +52,7 @@ + formControlName="timestampFormat">
-
-
scada.behavior.behavior
- -
- - - -
{{ behaviorGroup.title | customTranslate }}
-
-
- - - -
-
- - - -
-
+ @if (behaviorGroups?.length) { +
+
scada.behavior.behavior
+ @for (behaviorGroup of behaviorGroups; track behaviorGroup) { + @if (behaviorGroup.title) { +
+ + + +
{{ behaviorGroup.title | customTranslate }}
+
+
+ + + +
+
+ } + @if (!behaviorGroup.title) { + + } + } +
+ } @@ -44,43 +48,49 @@
- + @for (behavior of behaviors; track behavior) { - + }
{{ behavior.name | customTranslate }}
- - - - - - + @if (behavior.type === ScadaSymbolBehaviorType.value) { + + + } + @if (behavior.type === ScadaSymbolBehaviorType.action) { + + + } + @if (behavior.type === ScadaSymbolBehaviorType.widgetAction) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html index c24ff79466..a2e07002a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html @@ -19,59 +19,67 @@
- - {{ valueSourceDataKeyTypeTranslation.get(type) | translate }} - + @for (type of valueSourceDataKeyTypes; track type) { + + {{ valueSourceDataKeyTypeTranslation.get(type) | translate }} + + } - - + @if (valueSourceFormGroup.get('type').value === valueSourceDataKeyType.entity) { + + + }
- - - - warning - - - - - - + @if (valueSourceFormGroup.get('type').value === valueSourceDataKeyType.constant) { + + + @if (valueSourceFormGroup.get('value').hasError('required') + && valueSourceFormGroup.get('value').touched) { + + warning + + } + + } + @if (valueSourceFormGroup.get('type').value === valueSourceDataKeyType.latestKey) { + + + } + @if (valueSourceFormGroup.get('type').value === valueSourceDataKeyType.entity) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html index b088489cc9..8721b060f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html @@ -40,16 +40,20 @@ #entityAliasInput formControlName="entityAlias" [matAutocomplete]="entityAliasAutocomplete"> - + @if (valueSourceFormGroup.get('entityAlias').value && !valueSourceFormGroup.get('entityAlias').disabled) { + + } - - - + @for (entityAlias of filteredEntityAliases | async; track entityAlias) { + + + + }
@@ -57,16 +61,20 @@ #keyInput formControlName="attribute" [matAutocomplete]="keyAutocomplete"> - + @if (valueSourceFormGroup.get('attribute').value && !valueSourceFormGroup.get('attribute').disabled) { + + } - - - + @for (key of filteredKeys | async; track key) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html index edba87c0db..4768ed4920 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html @@ -75,12 +75,13 @@ icon="format_color_fill" label="{{ 'widgets.widget-font.color' | translate }}" openOnInput colorClearButton> - - + @if (hasShadowColor) { + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html index 65f2044a7b..219ef4485c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html @@ -17,9 +17,13 @@ -->
-
{{definedDirectiveError}}
- - + @if (definedDirectiveError) { +
{{definedDirectiveError}}
+ } + @if (useDynamicForm()) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html index 5882b18996..41931d50cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html @@ -19,39 +19,48 @@ [subscriptSizing]="inlineField ? 'dynamic' : 'fixed'" [class]="{'tb-inline-field': inlineField, 'flex': inlineField, 'tb-suffix-absolute': (inlineField && !deviceKeyFormGroup.get('key').value)}" - [formGroup]="deviceKeyFormGroup"> - {{ (keyType === dataKeyType.attribute - ? attributeLabel + [formGroup]="deviceKeyFormGroup"> + @if (!inlineField) { + {{ (keyType === dataKeyType.attribute + ? attributeLabel : timeseriesLabel) | translate }} + } - - - warning - + @if (deviceKeyFormGroup.get('key').value && !deviceKeyFormGroup.get('key').disabled) { + + } + @if (inlineField && requiredText && deviceKeyFormGroup.get('key').hasError('required') + && deviceKeyFormGroup.get('key').touched) { + + warning + + } - - - + @for (key of filteredKeys | async; track key) { + + + + } - - {{ requiredText | translate }} - + @if (!inlineField && requiredText && deviceKeyFormGroup.get('key').hasError('required')) { + + {{ requiredText | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html index 76929a5394..4dda766e04 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html @@ -55,34 +55,36 @@ - - warning - + class="tb-error"> + warning + + }
widgets.table.page-step-count
- - warning - + class="tb-error"> + warning + + } @@ -90,9 +92,11 @@
widgets.table.default-page-size
- - {{ size }} - + @for (size of pageStepSizeValues; track size) { + + {{ size }} + + } @@ -112,11 +116,13 @@ cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="columnDrop($event)"> - - {{ displayColumnFromValue(column)?.name }} - cancel - + @for (column of persistentTableWidgetSettingsForm.get('displayColumns').value; track column) { + + {{ displayColumnFromValue(column)?.name }} + cancel + + } - - - - -
-
- widgets.persistent-table.no-columns-found -
- + @for (column of filteredDisplayColumns | async; track column) { + + + + } + @if ((filteredDisplayColumns | async)?.length === 0) { + +
+ @if (!textIsNotEmpty(columnSearchText)) { +
+ widgets.persistent-table.no-columns-found +
+ } @else { {{ translate.get('widgets.persistent-table.no-columns-matching', {column: truncate.transform(columnSearchText, true, 6, '...')}) | async }} - -
-
+ } +
+
+ } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html index 330433141e..e34b9ae2a0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html @@ -29,7 +29,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.rpc-state.turn-on
@@ -38,7 +38,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="onUpdateState"> + formControlName="onUpdateState">
widgets.rpc-state.turn-off
@@ -47,7 +47,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="offUpdateState"> + formControlName="offUpdateState">
widgets.rpc-state.disabled-state
@@ -58,7 +58,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -69,12 +69,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> - - {{ singleSwitchLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.single-switch.layout' | translate }}" formControlName="layout"> + @for (layout of singleSwitchLayouts; track layout) { + + {{ singleSwitchLayoutTranslationMap.get(layout) | translate }} + + }
@@ -87,15 +89,15 @@
+ appearance="outline" + brackets="curly" + subscriptSizing="dynamic" + predefinedValuesButton="true" + [disabled]="!singleSwitchWidgetSettingsForm.get('showLabel').value" + [predefinedValues]="predefinedValues" + placeholderText="{{ 'widget-config.set' | translate }}" + formControlName="label" + > diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html index 5d1d987a76..01edd30eb3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html @@ -26,7 +26,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.slider.on-value-change
@@ -36,7 +36,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="valueChange"> + formControlName="valueChange">
widgets.rpc-state.disabled-state
@@ -47,7 +47,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -58,12 +58,14 @@ 'lt-sm': 1, 'lt-md': 2 }}" - label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout"> - - {{ sliderLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout"> + @for (layout of sliderLayouts; track layout) { + + {{ sliderLayoutTranslationMap.get(layout) | translate }} + + }
@@ -84,25 +86,27 @@
widgets.slider.slider
-
- - {{ 'widgets.slider.value' | translate }} - -
- - - -
widget-config.decimals-suffix
-
- - - - + @if (sliderWidgetSettingsForm.get('layout').value !== sliderLayout.simplified) { +
+ + {{ 'widgets.slider.value' | translate }} + +
+ + + +
widget-config.decimals-suffix
+
+ + + + +
-
+ }
{{ 'widgets.slider.range' | translate }}
@@ -180,43 +184,47 @@
-
-
- {{ 'widgets.slider.left-icon' | translate }} -
-
- - - - - - - - -
-
-
-
- {{ 'widgets.slider.right-icon' | translate }} + @if (sliderWidgetSettingsForm.get('layout').value === sliderLayout.extended) { +
+
+ {{ 'widgets.slider.left-icon' | translate }} +
+
+ + + + + + + + +
-
- - - - - - - - + } + @if (sliderWidgetSettingsForm.get('layout').value === sliderLayout.extended) { +
+
+ {{ 'widgets.slider.right-icon' | translate }} +
+
+ + + + + + + + +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html index bb81b8d5c2..0f30cc1434 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html @@ -26,7 +26,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="initialState"> + formControlName="initialState">
widgets.value-stepper.left-button-click
@@ -36,7 +36,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="leftButtonClick"> + formControlName="leftButtonClick">
widgets.value-stepper.right-button-click
@@ -46,7 +46,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="rightButtonClick"> + formControlName="rightButtonClick">
widgets.button-state.disabled-state
@@ -57,7 +57,7 @@ [aliasController]="aliasController" [targetDevice]="targetDevice" [widgetType]="widgetType" - formControlName="disabledState"> + formControlName="disabledState">
@@ -67,12 +67,14 @@ breakpoints: { 'lt-sm': 1 }}" - label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> - - {{ valueStepperTypeTranslationMap.get(type) | translate }} - + label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> + @for (type of valueStepperTypes; track type) { + + {{ valueStepperTypeTranslationMap.get(type) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html index eb36eafb06..8950de48f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html @@ -20,14 +20,14 @@
widgets.table.table-header
{{ 'widgets.table.entities-table-title' | translate }}
-
@@ -104,34 +104,36 @@ - - warning - + class="tb-error"> + warning + + }
widgets.table.page-step-count
- - warning - + class="tb-error"> + warning + + }
@@ -139,9 +141,11 @@
widgets.table.default-page-size
- - {{ size }} - + @for (size of pageStepSizeValues; track size) { + + {{ size }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html index 1bebce0b7d..5df07f2820 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html @@ -16,8 +16,8 @@ -->
- widgets.gateway.events-filter - - {{eventFilter}} - cancel - + @for (eventFilter of gatewayEventsWidgetSettingsForm.get('eventsReg').value; track eventFilter) { + + {{eventFilter}} + cancel + + } - - + @switch (gaugeType) { + @case ('radial') { - - + } + @case ('linear') { - - + } + }
widgets.gauge.ticks-settings
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html index 29d9a65d17..3decf4f45f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html @@ -23,21 +23,25 @@ breakpoints: { 'lt-md': 2 }}" - label="{{ 'widgets.gauge.simple-gauge-type' | translate }}" - formControlName="gaugeType"> - - {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.gauge.simple-gauge-type' | translate }}" + formControlName="gaugeType"> + @for (layout of digitalGaugeLayouts; track layout) { + + {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} + + } -
-
{{ 'widgets.gauge.donut-start-angle' | translate }}
- - - -
+ @if (digitalGaugeWidgetSettingsForm.get('gaugeType').value === digitalGaugeType.donut) { +
+
{{ 'widgets.gauge.donut-start-angle' | translate }}
+ + + +
+ }
@@ -65,14 +69,15 @@
widgets.gauge.max-value-short
- - warning - + @if (digitalGaugeWidgetSettingsForm.get('maxValue').hasError('maxValue')) { + + warning + + }
@@ -227,8 +232,8 @@
widgets.gauge.tick-values
-
+ @for (tickValueControl of tickValuesFormArray().controls; track trackByTickValue($index, tickValueControl); let isLast = $last) { +
drag_indicator -
-
-
- widgets.gauge.no-tick-values +
+ }
+ @if (!tickValuesFormArray().controls.length) { +
+ widgets.gauge.no-tick-values +
+ }
+ @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html index 9a00b3bb4c..8863b76249 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html @@ -28,19 +28,22 @@ widgets.gpio.gpio-switches
-
- - -
+ @for (gpioItemControl of typedSelectOptions; track trackByGpioItem($index, gpioItemControl); let isLast = $last) { +
+ + +
+ }
-
+ @if (!gpioListFormArray().controls.length) { +
widgets.gpio.no-gpio-switches -
+ class="tb-prompt flex items-center justify-center">widgets.gpio.no-gpio-switches +
+ }
-
-
-
+ @if (hasColor) { +
+
+
+ }
- + @if (!disabled) { + + }
@@ -65,13 +69,14 @@ - - + @if (hasColor) { + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html index 1b7ff3d4e5..f002322bfe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html @@ -28,20 +28,23 @@ widgets.gpio.gpio-leds
-
- - -
+ @for (gpioItemControl of typedSelectOptions; track trackByGpioItem($index, gpioItemControl); let isLast = $last) { +
+ + +
+ }
-
+ @if (!gpioListFormArray().controls.length) { +
widgets.gpio.no-gpio-leds -
+ class="tb-prompt flex items-center justify-center">widgets.gpio.no-gpio-leds +
+ }
+ @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html index efa5b6cd3a..f23e000981 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html @@ -18,23 +18,25 @@
widgets.input-widgets.general-settings -
{{ 'widgets.input-widgets.show-label' | translate }} - - widgets.input-widgets.label - - + @if (hasLabelValue) { + + widgets.input-widgets.label + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html index 6419028708..4d3b384386 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html @@ -102,31 +102,33 @@
-
- - widgets.input-widgets.field-appearance - - - {{ 'widgets.input-widgets.appearance-fill' | translate }} - - - {{ 'widgets.input-widgets.appearance-outline' | translate }} - - - - - widgets.input-widgets.subscript-sizing - - - {{ 'widgets.input-widgets.subscript-sizing-fixed' | translate }} - - - {{ 'widgets.input-widgets.subscript-sizing-dynamic' | translate }} - - - -
+ @if (!['booleanSwitch', 'booleanCheckbox', 'radio'].includes(updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value)) { +
+ + widgets.input-widgets.field-appearance + + + {{ 'widgets.input-widgets.appearance-fill' | translate }} + + + {{ 'widgets.input-widgets.appearance-outline' | translate }} + + + + + widgets.input-widgets.subscript-sizing + + + {{ 'widgets.input-widgets.subscript-sizing-fixed' | translate }} + + + {{ 'widgets.input-widgets.subscript-sizing-dynamic' | translate }} + + + +
+ }
-
- - -
-
-
- - {{ (updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value === 'select' ? 'widgets.input-widgets.no-select-options' : 'widgets.input-widgets.no-radio-options') | translate }} - + @for (selectOptionControl of typedSelectOptions; track trackBySelectOption($index, selectOptionControl); let isLast = $last) { +
+ + +
+ }
+ @if (!selectOptionsFormArray().controls.length) { +
+ + {{ (updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value === 'select' ? 'widgets.input-widgets.no-select-options' : 'widgets.input-widgets.no-radio-options') | translate }} + +
+ }
+ class="fields-group"> widgets.input-widgets.icon-settings {{ 'widgets.input-widgets.use-custom-icon' | translate }} @@ -286,8 +290,8 @@ formControlName="icon"> + label="{{ 'widgets.input-widgets.input-cell-icon' | translate }}" + formControlName="customIcon">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts index c007c51ef8..0212ee4279 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts @@ -256,6 +256,10 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings return this.updateMultipleAttributesKeySettingsForm.get('selectOptions') as UntypedFormArray; } + get typedSelectOptions() { + return this.selectOptionsFormArray().controls as (AbstractControl & { new?: boolean })[]; + } + public trackBySelectOption(index: number, selectOptionControl: AbstractControl): any { return selectOptionControl; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html index f13de79d27..1a8fb6a328 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/circle-settings.component.html @@ -88,9 +88,11 @@ widgets.maps.show-tooltip-action - - {{showTooltipActionTranslations.get(action) | translate}} - + @for (action of showTooltipActions; track action) { + + {{showTooltipActionTranslations.get(action) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.html index 247a92f7ba..b0d8c742f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/datasources-key-autocomplete.component.html @@ -23,17 +23,21 @@ (focusin)="onFocus()" formControlName="key" [matAutocomplete]="keyAutocomplete"> - + @if (keyFormGroup.get('key').value && !keyFormGroup.get('key').disabled) { + + } - - - + @for (key of filteredKeys | async; track key) { + + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.html index 7ffaff50c4..49b0e479c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/google-map-provider-settings.component.html @@ -23,9 +23,11 @@ widgets.maps.default-map-type - - {{googleMapTypeTranslations.get(type) | translate}} - + @for (type of googleMapTypes; track type) { + + {{googleMapTypeTranslations.get(type) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.html index 056be11115..6a052ac907 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/here-map-provider-settings.component.html @@ -19,9 +19,11 @@ widgets.maps.map-layer - - {{hereMapProviderTranslations.get(provider) | translate}} - + @for (provider of hereMapProviders; track provider) { + + {{hereMapProviderTranslations.get(provider) | translate}} + + }
@@ -30,22 +32,23 @@ {{ 'widgets.maps.here-use-new-version-api-3' | translate }} -
- - widgets.maps.here-app-id - - - - widgets.maps.here-app-code - - -
- + @if (!providerSettingsFormGroup.get('credentials.useV3').value) { +
+ + widgets.maps.here-app-id + + + + widgets.maps.here-app-code + + +
+ } @else { widgets.maps.here-api-key -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.html index 6182b2b1fa..628d4853e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/image-map-provider-settings.component.html @@ -17,7 +17,7 @@ -->
+ formControlName="mapImageUrl">
widgets.maps.image-map-background-from-entity-attribute @@ -30,18 +30,22 @@ (focusin)="onEntityAliasFocus()" formControlName="imageEntityAlias" [matAutocomplete]="entityAliasAutocomplete"> - + @if (providerSettingsFormGroup.get('imageEntityAlias').value && !providerSettingsFormGroup.get('imageEntityAlias').disabled) { + + } - - - + @for (entityAlias of filteredEntityAliases | async; track entityAlias) { + + + + } @@ -52,18 +56,22 @@ (focusin)="onKeyFocus()" formControlName="imageUrlAttribute" [matAutocomplete]="keyAutocomplete"> - + @if (providerSettingsFormGroup.get('imageUrlAttribute').value && !providerSettingsFormGroup.get('imageUrlAttribute').disabled) { + + } - - - + @for (key of filteredKeys | async; track key) { + + + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.html index b2d334a621..8566956dc0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/map-provider-settings.component.html @@ -21,9 +21,11 @@ widgets.maps.map-provider - - {{mapProviderTranslations.get(provider) | translate}} - + @for (provider of mapProviders; track provider) { + + {{mapProviderTranslations.get(provider) | translate}} + + } - - - - + @if (!routeMap) { + + + } + @if (routeMap) { + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html index b786ea700e..609cfea4b8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/markers-settings.component.html @@ -90,9 +90,11 @@ widgets.maps.show-tooltip-action - - {{showTooltipActionTranslations.get(action) | translate}} - + @for (action of showTooltipActions; track action) { + + {{showTooltipActionTranslations.get(action) | translate}} + + } @@ -171,8 +173,8 @@ + label="{{ 'widgets.maps.custom-marker-image' | translate }}" + formControlName="markerImage"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.html index a08823395d..8fb0b3c705 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/openstreet-map-provider-settings.component.html @@ -19,9 +19,11 @@ widgets.maps.openstreet-provider - - {{openStreetMapProviderTranslations.get(provider) | translate}} - + @for (provider of mapProviders; track provider) { + + {{openStreetMapProviderTranslations.get(provider) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.html index 3d82204a26..03bb018528 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/polygon-settings.component.html @@ -88,9 +88,11 @@ widgets.maps.show-tooltip-action - - {{showTooltipActionTranslations.get(action) | translate}} - + @for (action of showTooltipActions; track action) { + + {{showTooltipActionTranslations.get(action) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.html index 88c36ab010..ccde76edad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/tencent-map-provider-settings.component.html @@ -23,9 +23,11 @@ widgets.maps.default-map-type - - {{tencentMapTypeTranslations.get(type) | translate}} - + @for (type of tencentMapTypes; track type) { + + {{tencentMapTypeTranslations.get(type) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.html index 6034cbfc5c..57e4cfb873 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/legacy/trip-animation-path-settings.component.html @@ -73,9 +73,11 @@ widgets.maps.decorator-symbol - - {{polylineDecoratorSymbolTranslations.get(symbol) | translate}} - + @for (symbol of polylineDecoratorSymbols; track symbol) { + + {{polylineDecoratorSymbolTranslations.get(symbol) | translate}} + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html index d8545078c1..d240db2726 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html @@ -27,11 +27,13 @@ widgets.navigation.items - - {{ filterItem }} - cancel - + @for (filterItem of navigationCardsWidgetSettingsForm.get('filter').value; track filterItem) { + + {{ filterItem }} + cancel + + } - - - + @for (filterItem of filteredFilterItems | async; track filterItem) { + + + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html index fa9c964654..230db9e857 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html @@ -23,12 +23,14 @@ breakpoints: { 'lt-md': 2 }}" - label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout"> - - {{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }} - + label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout"> + @for (layout of windSpeedDirectionLayouts; track layout) { + + {{ windSpeedDirectionLayoutTranslationMap.get(layout) | translate }} + + }
{{ (hasCenterValue ? 'widgets.wind-speed-direction.center-value' : 'widgets.wind-speed-direction.wind-direction') | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 99a31331fd..a744ac54a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -41,90 +41,108 @@ class="flex-1" mat-stretch-tabs="false" [(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()"> - - -
- - - {{ 'widgets.table.timestamp-column-name' | translate }} - - - - - {{ h.dataKey.label }} - - - - - - -
-
-
-
+ @for (source of sources; track trackBySourcesIndex(index, source); let index = $index) { + + @if (isActiveTab(index)) { +
+
+ @if (showTimestamp) { + + {{ 'widgets.table.timestamp-column-name' | translate }} + + - - - -
- - - - -
-
- - - - + } + } +
+ @if (row.hasActions) { +
+ - - -
-
-
- - - -
- {{ noDataDisplayMessageText }} -
- - -
-
+ + @for (actionDescriptor of row.actionCellButtons; track trackByActionCellDescriptionId($index, actionDescriptor)) { + @if (actionDescriptor.icon) { + + } + } + +
+ } + } + +
+ + + + {{ noDataDisplayMessageText }} + + @if (displayPagination) { + + } + @if (displayPagination) { + + } + } + + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html index a41bf2ed82..a5a7bb955b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html @@ -16,32 +16,40 @@ -->
-
-
-
-
- -
-
-
-
+ @if (settings.showLabel) { +
+ } +
+
+
+ @if (settings.showTooltip) { + + } +
+
+ @for (mainTooltip of mainTooltips; track mainTooltip) { +
+ }
- +
+ @if (historicalData) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 166c32a7fd..3b248f086e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -16,222 +16,240 @@ -->
-
- - -
- - -
-
-
-
- -
-
- + @if (!hideHeader) { +
+ @if (!hideToggleHeader && widgetConfigMode === widgetConfigModes.advanced) { + + + } +
+ + +
-
-
-
widget-config.card-title
- - {{ 'widget-config.display-title' | translate }} - -
-
widget-config.title
-
- @if (widgetType === widgetTypes.static || widgetType === widgetTypes.alarm) { - - + } + @if (widgetConfigMode === widgetConfigModes.advanced) { +
+ @if (displayData) { +
+ +
+ } +
+ +
+
+
+
widget-config.card-title
+ + {{ 'widget-config.display-title' | translate }} + +
+
widget-config.title
+
+ @if (widgetType === widgetTypes.static || widgetType === widgetTypes.alarm) { + + + + } @else { + + } + + + + +
+
+
+
widget-config.title-tooltip
+ + + +
+
+ + {{ 'widget-config.display-icon' | translate }} + +
+ + - } @else { - - } - - + + + + +
+
+ + + + widget-config.advanced-title-style + + + + + + +
+
+
widget-config.card-style
+
+
{{ 'widget-config.text-color' | translate }}
+ formControlName="color">
-
-
-
widget-config.title-tooltip
- - - -
-
- - {{ 'widget-config.display-icon' | translate }} - -
- - - - - +
+
{{ 'widget-config.background-color' | translate }}
+ formControlName="backgroundColor">
-
- - - - widget-config.advanced-title-style - - - - - - -
-
-
widget-config.card-style
-
-
{{ 'widget-config.text-color' | translate }}
- - -
-
-
{{ 'widget-config.background-color' | translate }}
- - -
-
-
{{ 'widget-config.padding' | translate }}
- - - -
-
-
{{ 'widget-config.margin' | translate }}
- - - -
-
-
{{ 'widget-config.border-radius' | translate }}
- - - -
- - {{ 'widget-config.drop-shadow' | translate }} - - - - - widget-config.advanced-widget-style - - - - - - - -
-
-
widget-config.card-buttons
- - {{ 'widget-config.enable-fullscreen' | translate }} - -
-
-
-
- - -
-
-
-
-
widget-config.resize-options
-
- - {{ 'widget-config.resizable' | translate }} +
+
{{ 'widget-config.padding' | translate }}
+ + + +
+
+
{{ 'widget-config.margin' | translate }}
+ + + +
+
+
{{ 'widget-config.border-radius' | translate }}
+ + + +
+ + {{ 'widget-config.drop-shadow' | translate }} + + + + widget-config.advanced-widget-style + + + + + + +
-
- - {{ 'widget-config.preserve-aspect-ratio' | translate }} +
+
widget-config.card-buttons
+ + {{ 'widget-config.enable-fullscreen' | translate }}
-
-
{{ (isDefaultBreakpoint ? 'widget-config.mobile' : 'widget-config.list-layout') | translate }}
-
- - {{ 'widget-config.mobile-hide' | translate }} - -
-
- - {{ 'widget-config.desktop-hide' | translate }} - -
-
-
widget-config.order
- - - -
-
-
widget-config.height
- - - +
+
+ +
+
+ @if (!showLayoutConfig || isDefaultBreakpoint) { +
+
widget-config.resize-options
+
+ + {{ 'widget-config.resizable' | translate }} + +
+
+ + {{ 'widget-config.preserve-aspect-ratio' | translate }} + +
+
+ } + @if (showLayoutConfig) { +
+
{{ (isDefaultBreakpoint ? 'widget-config.mobile' : 'widget-config.list-layout') | translate }}
+ @if (isDefaultBreakpoint) { +
+ + {{ 'widget-config.mobile-hide' | translate }} + +
+ } + @if (isDefaultBreakpoint) { +
+ + {{ 'widget-config.desktop-hide' | translate }} + +
+ } +
+
widget-config.order
+ + + +
+
+
widget-config.height
+ + + +
+
+ } +
-
- + } @else {
-
- -
{{basicModeDirectiveError}}
-
- + @if (hasBasicModeDirective) { +
+ + @if (basicModeDirectiveError) { +
{{basicModeDirectiveError}}
+ } +
+ } @else {
@@ -241,98 +259,122 @@
-
+ }
-
+ } - - - - -
-
-
alarm.filter
- + @if (displayTimewindowConfig) { + + + + + } + @if (widgetType === widgetTypes.alarm) { +
+
+
alarm.filter
+ +
+
- -
-
- - -
- - - - -
-
widget-config.alarm-source
- - -
-
-
widget-config.limits
-
-
widget-config.data-page-size
- - - + } + @if (widgetType !== widgetTypes.rpc && + widgetType !== widgetTypes.alarm && + modelValue?.isDataEnabled) { +
+ +
-
+ } + @if (widgetType === widgetTypes.rpc && + modelValue?.isDataEnabled) { + + + + + } + @if (widgetType === widgetTypes.alarm && modelValue?.isDataEnabled) { +
+
widget-config.alarm-source
+ + +
+ } + @if (displayLimits) { +
+
widget-config.limits
+
+
widget-config.data-page-size
+ + + +
+
+ } - - - - -
-
widget-config.data-settings
-
-
widget-config.units-by-default
- - -
-
-
widget-config.decimals-by-default
- - - + @if (!displayData && displayTimewindowConfig) { + + + + + } + @if (displayAppearanceDataSettings) { +
+
widget-config.data-settings
+ @if (displayUnitsConfig) { +
+
widget-config.units-by-default
+ + +
+ } + @if (displayUnitsConfig) { +
+
widget-config.decimals-by-default
+ + + +
+ } + @if (displayNoDataDisplayMessageConfig) { +
+
widget-config.no-data-display-message
+ + + +
+ }
-
-
widget-config.no-data-display-message
- - - + } + @if (displayAdvancedAppearance) { +
+ +
-
-
- - -
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index e182371f2b..69cf1de06a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -28,15 +28,17 @@ [class.tb-edit]="isEdit || isEditingWidget" [class.tb-hover]="hovered" [style]="widget.style"> -
- - - - - - -
+ @if (!!widgetComponent.widgetContext?.inited) { +
+ @if (!widgetComponent.widgetContext?.embedTitlePanel) { + + } + @if (!widgetComponent.widgetContext?.embedActionsPanel) { + + } +
+ }
-
-
- {{widget.titleIcon}} -
- {{widget.title$ | async}} -
- -
- - -
+ @if (widget.showWidgetTitlePanel) { +
+ @if (widget.showTitle) { +
+ @if (widget.showTitleIcon) { + {{widget.titleIcon}} + } +
+ {{widget.title$ | async}} +
+ +
+ } + @if (widget.hasTimewindow) { + + + } +
+ }
-
-
- @for (action of widget.customHeaderActions; track action.name; let last = $last) { - + @if (widget.showWidgetActions) { +
+
+ @for (action of widget.customHeaderActions; track action.name; let last = $last) { + + } +
+ @for (action of widget.widgetActions; track action) { + } +
- - - -
+ } -@switch (action.buttonType) { - @case (widgetHeaderActionButtonType.miniFab) { - - } - @case (widgetHeaderActionButtonType.basic) { - - } - @case (widgetHeaderActionButtonType.raised) { - - } - @case (widgetHeaderActionButtonType.stroked) { - - } - @case (widgetHeaderActionButtonType.flat) { - - } - @default { - + @switch (action.buttonType) { + @case (widgetHeaderActionButtonType.miniFab) { + + } + @case (widgetHeaderActionButtonType.basic) { + + } + @case (widgetHeaderActionButtonType.raised) { + + } + @case (widgetHeaderActionButtonType.stroked) { + + } + @case (widgetHeaderActionButtonType.flat) { + + } + @default { + + } } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index 3553ee3ae4..4bdceedf7c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -397,37 +397,39 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O @Component({ template: `
-
- {{ 'widget.reference' | translate }} - -
+ @if (container.widget.isReference) { +
+ {{ 'widget.reference' | translate }} + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html index b0dbb95937..c66578afac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -21,13 +21,17 @@
-
+@if (widgetErrorData) { +
-
- {{ noDataDisplayMessageText }} -
+} +@if (displayNoData) { +
+ {{ noDataDisplayMessageText }} +
+}
diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html index 06f99fc498..eb07e82ce8 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
{{ assignToCustomerText | translate }} @@ -35,7 +39,7 @@ formControlName="entityIds" required [entityType]="entityType" - > + >
diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html index 6a4be4aeb4..c95d297864 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
{{ assignToEdgeText | translate }} diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html index 4af3aa07a1..3344e70837 100644 --- a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
{{ assignToCustomerText | translate }}
diff --git a/ui-ngx/src/app/modules/home/home.component.html b/ui-ngx/src/app/modules/home/home.component.html index 690c1f344c..81d0ae2e6d 100644 --- a/ui-ngx/src/app/modules/home/home.component.html +++ b/ui-ngx/src/app/modules/home/home.component.html @@ -51,8 +51,8 @@ arrow_back + [class.!hidden]="displaySearchMode()" + class="mat-toolbar-tools flex-1">
@@ -66,18 +66,22 @@ mat-icon-button (click)="openSearch()"> search - + @if (fullscreenEnabled) { + + } - - + @if (!hideLoadingBar && (isLoading$ | async)) { + + + }
diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.html b/ui-ngx/src/app/modules/home/menu/menu-link.component.html index 3d3823d12a..70ea95a05f 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.html @@ -16,6 +16,8 @@ --> - {{section.icon}} + @if (section.icon !== null) { + {{section.icon}} + } {{section.customTranslate ? (section.name | customTranslate) : (section.name | translate)}} diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html index 31753751af..ce475363df 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -16,13 +16,17 @@ --> - {{section.icon}} + @if (section.icon !== null) { + {{section.icon}} + } {{section.customTranslate ? (section.name | customTranslate) : (section.name | translate)}} + [class.tb-toggled]="section.opened">
    -
  • - -
  • + @for (page of section.pages; track trackBySectionPages($index, page)) { +
  • + +
  • + }
diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.html b/ui-ngx/src/app/modules/home/menu/side-menu.component.html index 0921a59f14..34304319ae 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.html +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.html @@ -16,8 +16,16 @@ -->
    -
  • - - -
  • + @for (section of menuSections$ | async; track trackByMenuSection($index, section)) { +
  • + @switch (section.type === 'link') { + @case (true) { + + } + @case (false) { + + } + } +
  • + }
diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index 336ac035da..f870150e32 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -51,6 +51,9 @@ export type HeaderCellStyleFunction> = (key: string) = export type CellStyleFunction> = (entity: T, key: string) => object; export type CopyCellContent> = (entity: T, key: string, length: number) => object; +export type EntityColumnsType = Array> & EntityLinkTableColumn> & EntityChipsEntityTableColumn>>>; +export type EntityColumnType = Partial> & EntityLinkTableColumn> & EntityChipsEntityTableColumn>>; + export enum CellActionDescriptorType { 'DEFAULT', 'COPY_BUTTON'} export interface CellActionDescriptor> { diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index 02f120f26f..e913e2bd71 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -29,6 +29,7 @@ import { CellActionDescriptor, EntityActionTableColumn, EntityColumn, + EntityColumnsType, EntityTableColumn, EntityTableConfig, GroupActionDescriptor, @@ -46,7 +47,7 @@ export interface IEntitiesTableComponent { groupActionDescriptors: Array>>; cellActionDescriptors: Array>>; actionColumns: Array>>; - entityColumns: Array>>; + entityColumns: EntityColumnsType; displayedColumns: string[]; headerCellStyleCache: Array; cellContentCache: Array; diff --git a/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html index c18d59a5c8..9948f20cf7 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html @@ -15,9 +15,10 @@ limitations under the License. --> - - - +@if (!(hasRepository$ | async)) { + + +} @else { - +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html index 4b122194e8..4c9971bb42 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html @@ -23,9 +23,13 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
@@ -33,9 +37,11 @@ admin.base-url - - {{ 'admin.base-url-required' | translate }} - + @if (generalSettings.get('baseUrl').hasError('required')) { + + {{ 'admin.base-url-required' | translate }} + + }
@@ -63,9 +69,13 @@
admin.device-connectivity.device-connectivity
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
@@ -78,18 +88,18 @@
- + @if (protocol === 'http') { - - + } + @if (protocol === 'mqtt') { - - + } + @if (protocol === 'coap') { - + }
@@ -103,13 +113,17 @@ admin.device-connectivity.port - - {{ 'admin.device-connectivity.port-pattern' | translate }} - - - {{ 'admin.device-connectivity.port-range' | translate }} - + @if (deviceConnectivitySettingsForm.get(protocol + '.port').hasError('pattern')) { + + {{ 'admin.device-connectivity.port-pattern' | translate }} + + } + @if (deviceConnectivitySettingsForm.get(protocol + '.port').hasError('min') || + deviceConnectivitySettingsForm.get(protocol + '.port').hasError('max')) { + + {{ 'admin.device-connectivity.port-range' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html index 9ebfd0369b..a653845ca3 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html @@ -22,9 +22,13 @@ admin.home-settings - - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html index 2ba7adda3b..cea12d5ef8 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html @@ -24,26 +24,34 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
admin.mail-from - - {{ 'admin.mail-from-required' | translate }} - + @if (mailSettings.get('mailFrom').hasError('required')) { + + {{ 'admin.mail-from-required' | translate }} + + } admin.oauth2.smtp-provider - - {{ templates.get(provider)?.name || 'Custom' }} - + @for (provider of templateProvider; track provider) { + + {{ templates.get(provider)?.name || 'Custom' }} + + } @@ -57,90 +65,114 @@ admin.smtp-protocol - - {{protocol.toUpperCase()}} - + @for (protocol of smtpProtocols; track protocol) { + + {{protocol.toUpperCase()}} + + }
admin.smtp-host - - {{ 'admin.smtp-host-required' | translate }} - + @if (mailSettings.get('smtpHost').hasError('required')) { + + {{ 'admin.smtp-host-required' | translate }} + + } admin.smtp-port {{smtpPortInput.value?.length || 0}}/5 - - {{ 'admin.smtp-port-required' | translate }} - - - {{ 'admin.smtp-port-invalid' | translate }} - + @if (mailSettings.get('smtpPort').hasError('required')) { + + {{ 'admin.smtp-port-required' | translate }} + + } + @if (mailSettings.get('smtpPort').hasError('pattern') || mailSettings.get('smtpPort').hasError('maxlength')) { + + {{ 'admin.smtp-port-invalid' | translate }} + + }
admin.timeout-msec {{timeoutInput.value?.length || 0}}/6 - - {{ 'admin.timeout-required' | translate }} - - - {{ 'admin.timeout-invalid' | translate }} - + @if (mailSettings.get('timeout').hasError('required')) { + + {{ 'admin.timeout-required' | translate }} + + } + @if (mailSettings.get('timeout').hasError('pattern') || mailSettings.get('timeout').hasError('maxlength')) { + + {{ 'admin.timeout-invalid' | translate }} + + } {{ 'admin.enable-tls' | translate }} - - admin.tls-version - - - {{ tlsVersion }} - - - + @if (mailSettings.get('enableTls').value) { + + admin.tls-version + + @for (tlsVersion of tlsVersions; track tlsVersion) { + + {{ tlsVersion }} + + } + + + } {{ 'admin.enable-proxy' | translate }} -
-
- - admin.proxy-host - - - {{ 'admin.proxy-host-required' | translate }} - - - - admin.proxy-port - - - {{ 'admin.proxy-port-required' | translate }} - - - {{ 'admin.proxy-port-range' | translate }} - - -
-
- - admin.proxy-user - - - - admin.proxy-password - - - + @if (mailSettings.get('enableProxy').value) { +
+
+ + admin.proxy-host + + @if (mailSettings.get('proxyHost').hasError('required')) { + + {{ 'admin.proxy-host-required' | translate }} + + } + + + admin.proxy-port + + @if (mailSettings.get('proxyPort').hasError('required')) { + + {{ 'admin.proxy-port-required' | translate }} + + } + @if (mailSettings.get('proxyPort').hasError('pattern') + || mailSettings.get('proxyPort').hasError('min') + || mailSettings.get('proxyPort').hasError('max')) { + + {{ 'admin.proxy-port-range' | translate }} + + } + +
+
+ + admin.proxy-user + + + + admin.proxy-password + + + +
-
+ } @@ -156,161 +188,196 @@ {{ 'admin.oauth2.basic' | translate }} {{ 'admin.oauth2.oauth2' | translate }} -
-
-
+ @if (helpLink && mailSettings.get('enableOauth2').value) { +
+
+
+ }
-
- - {{ 'admin.change-password' | translate }} - - - common.password - - - -
-
-
- - admin.oauth2.client-id - - - {{ 'admin.oauth2.client-id-required' | translate }} - - - {{ 'admin.oauth2.client-id-max-length' | translate }} - - - - - admin.oauth2.client-secret - - - {{ 'admin.oauth2.client-secret-required' | translate }} - - - {{ 'admin.oauth2.client-secret-max-length' | translate }} - - -
- - - admin.oauth2.microsoft-tenant-id - - - {{ 'admin.oauth2.microsoft-tenant-id-required' | translate }} - - - - - - tenant-profile.advanced-settings - - - -
- - admin.oauth2.authorization-uri - - - - {{ 'admin.oauth2.access-token-uri-required' | translate }} + @if (!mailSettings.get('enableOauth2').value) { +
+ @if (showChangePassword) { + + {{ 'admin.change-password' | translate }} + + } + @if (mailSettings.get('changePassword').value || !showChangePassword) { + + common.password + + + + } +
+ } + @if (mailSettings.get('enableOauth2').value) { +
+
+ + admin.oauth2.client-id + + @if (mailSettings.get('clientId').hasError('required')) { + + {{ 'admin.oauth2.client-id-required' | translate }} - - {{ 'admin.oauth2.uri-pattern-error' | translate }} + } + @if (mailSettings.get('clientId').hasError('maxlen gth')) { + + {{ 'admin.oauth2.client-id-max-length' | translate }} - - - - admin.oauth2.token-uri - - - - {{ 'admin.oauth2.access-token-uri-required' | translate }} + } + + + admin.oauth2.client-secret + + @if (mailSettings.get('clientSecret').hasError('required')) { + + {{ 'admin.oauth2.client-secret-required' | translate }} - - {{ 'admin.oauth2.uri-pattern-error' | translate }} + } + @if (mailSettings.get('clientSecret').hasError('maxlength')) { + + {{ 'admin.oauth2.client-secret-max-length' | translate }} - -
- - - - - -
- admin.oauth2.redirect-uri -
-
-
- - admin.oauth2.protocol - - - {{ domainSchemaTranslations.get(protocol) | translate | uppercase }} - - + } + +
+ @if (mailSettings.get('providerId').value === mailServerOauth2Provider.OFFICE_365) { + + admin.oauth2.microsoft-tenant-id + + @if (mailSettings.get('providerTenantId').hasError('required')) { + + {{ 'admin.oauth2.microsoft-tenant-id-required' | translate }} + + } + + } + + + + tenant-profile.advanced-settings + + + +
+ + admin.oauth2.authorization-uri + + + @if (mailSettings.get('authUri').hasError('required')) { + + {{ 'admin.oauth2.access-token-uri-required' | translate }} + + } + @if (mailSettings.get('authUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + } - admin.domain-name - - - {{ 'admin.error-verification-url' | translate }} - - - {{ 'admin.domain-name-max-length' | translate }} + admin.oauth2.token-uri + + + @if (mailSettings.get('tokenUri').hasError('required')) { + + {{ 'admin.oauth2.access-token-uri-required' | translate }} + + } + @if (mailSettings.get('tokenUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + } + +
+ + +
+
+
+ admin.oauth2.redirect-uri +
+
+
+ + admin.oauth2.protocol + + @for (protocol of protocols; track protocol) { + + {{ domainSchemaTranslations.get(protocol) | translate | uppercase }} + + } + + + + admin.domain-name + + @if (domainForm.get('name').hasError('pattern')) { + + {{ 'admin.error-verification-url' | translate }} + + } + @if (domainForm.get('name').hasError('maxlength')) { + + {{ 'admin.domain-name-max-length' | translate }} + + } + +
+ @if (domainForm.hasError('unique')) { + + {{ 'admin.domain-name-unique' | translate }} + } +
+
+ + admin.oauth2.redirect-uri-template + + +
- - {{ 'admin.domain-name-unique' | translate }} - -
- -
- - admin.oauth2.redirect-uri-template - - - -
+
+
+ } + @if (mailSettings.get('enableOauth2').value) { +
+
+ admin.oauth2.access-token-status + + {{ accessTokenStatus }} +
-
- -
-
- admin.oauth2.access-token-status - - {{ accessTokenStatus }} - -
- -
+ + + }
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html index 12f522de96..6c60e36cf1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html @@ -20,12 +20,16 @@ admin.oauth2.title - - {{ 'admin.oauth2.client-title-required' | translate }} - - - {{ 'admin.oauth2.client-title-max-length' | translate }} - + @if (entityForm.get('title').hasError('required')) { + + {{ 'admin.oauth2.client-title-required' | translate }} + + } + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'admin.oauth2.client-title-max-length' | translate }} + + }
@@ -33,18 +37,22 @@ admin.oauth2.provider - - {{ provider }} - + @for (provider of templateProvider; track provider) { + + {{ provider }} + + }
admin.oauth2.allowed-platforms - - {{ platformTypeTranslations.get(platform) | translate }} - + @for (platform of platformTypes; track platform) { + + {{ platformTypeTranslations.get(platform) | translate }} + + }
@@ -52,22 +60,30 @@ admin.oauth2.client-id - - {{ 'admin.oauth2.client-id-required' | translate }} - - - {{ 'admin.oauth2.client-id-max-length' | translate }} - + @if (entityForm.get('clientId').hasError('required')) { + + {{ 'admin.oauth2.client-id-required' | translate }} + + } + @if (entityForm.get('clientId').hasError('maxlength')) { + + {{ 'admin.oauth2.client-id-max-length' | translate }} + + } admin.oauth2.client-secret - - {{ 'admin.oauth2.client-secret-required' | translate }} - - - {{ 'admin.oauth2.client-secret-max-length' | translate }} - + @if (entityForm.get('clientSecret').hasError('required')) { + + {{ 'admin.oauth2.client-secret-required' | translate }} + + } + @if (entityForm.get('clientSecret').hasError('maxlength')) { + + {{ 'admin.oauth2.client-secret-max-length' | translate }} + + }
@@ -85,237 +101,297 @@ {{ 'admin.oauth2.mapper' | translate }}
-
-
- - admin.oauth2.access-token-uri - - - {{ 'admin.oauth2.access-token-uri-required' | translate }} - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - - - admin.oauth2.authorization-uri - - - {{ 'admin.oauth2.authorization-uri-required' | translate }} - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - -
-
- - admin.oauth2.jwk-set-uri - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - - - admin.oauth2.user-info-uri - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - -
-
- - admin.oauth2.client-authentication-method - - - {{ clientAuthenticationMethod | uppercase }} - - - -
-
- - admin.oauth2.login-button-label - - - {{ 'admin.oauth2.login-button-label-required' | translate }} - - -
-
admin.oauth2.login-button-icon
- - -
-
-
-
- - {{ 'admin.oauth2.allow-user-creation' | translate }} - -
-
- - {{ 'admin.oauth2.activate-user' | translate }} - -
-
- - -
-
-
- - admin.oauth2.user-name-attribute-name - - - {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} - - -
-
-
+ @if (generalSettingsMode) { +
+
- admin.oauth2.type - - - {{ mapperType }} - - - -
-
-
- - admin.oauth2.email-attribute-key - - - {{ 'admin.oauth2.email-attribute-key-required' | translate }} + admin.oauth2.access-token-uri + + @if (entityForm.get('accessTokenUri').hasError('required')) { + + {{ 'admin.oauth2.access-token-uri-required' | translate }} - - {{ 'admin.oauth2.email-attribute-key-max-length' | translate }} + } + @if (entityForm.get('accessTokenUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} - -
-
- - admin.oauth2.first-name-attribute-key - - - {{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} + } + + + admin.oauth2.authorization-uri + + @if (entityForm.get('authorizationUri').hasError('required')) { + + {{ 'admin.oauth2.authorization-uri-required' | translate }} - - - admin.oauth2.last-name-attribute-key - - - {{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} + } + @if (entityForm.get('authorizationUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} - -
-
- - admin.oauth2.tenant-name-strategy - - - {{ tenantNameStrategy }} - - - - - admin.oauth2.tenant-name-pattern - - - {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} + } + +
+
+ + admin.oauth2.jwk-set-uri + + @if (entityForm.get('jwkSetUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} - - {{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} + } + + + admin.oauth2.user-info-uri + + @if (entityForm.get('userInfoUri').hasError('pattern')) { + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + } + +
+
+ + admin.oauth2.client-authentication-method + + @for (clientAuthenticationMethod of clientAuthenticationMethods; track clientAuthenticationMethod) { + + {{ clientAuthenticationMethod | uppercase }} + + } + + +
+ @if (isCustomProvider()) { +
+ + admin.oauth2.login-button-label + + @if (entityForm.get('loginButtonLabel').hasError('required')) { + + {{ 'admin.oauth2.login-button-label-required' | translate }} + + } +
+
admin.oauth2.login-button-icon
+ + +
-
- - admin.oauth2.customer-name-pattern - - - {{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} - - + } +
+
+ + {{ 'admin.oauth2.allow-user-creation' | translate }} +
-
- - admin.oauth2.default-dashboard-name - - - {{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} - - - - {{ 'admin.oauth2.always-fullscreen' | translate}} +
+ + {{ 'admin.oauth2.activate-user' | translate }}
-
-
- - admin.oauth2.url - - - {{ 'admin.oauth2.url-required' | translate }} - - - {{ 'admin.oauth2.url-pattern' | translate }} - - - {{ 'admin.oauth2.url-max-length' | translate }} - - -
-
- - common.username - - - {{ 'admin.oauth2.username-max-length' | translate }} + + +
+ } + @if (!generalSettingsMode) { +
+
+ + admin.oauth2.user-name-attribute-name + + @if (entityForm.get('userNameAttributeName').hasError('required')) { + + {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} - + } + +
+
+
- common.password - - - {{ 'admin.oauth2.password-max-length' | translate }} - + admin.oauth2.type + + @for (mapperType of mapperTypes; track mapperType) { + + {{ mapperType }} + + } +
- - {{ 'admin.oauth2.send-token' | translate}} - + @if (entityForm.get('mapperConfig.type').value !== mapperType.CUSTOM && entityForm.get('mapperConfig.basic')) { +
+ @if (entityForm.get('mapperConfig.type').value !== mapperType.GITHUB) { +
+ + admin.oauth2.email-attribute-key + + @if (entityForm.get('mapperConfig.basic.emailAttributeKey').hasError('required')) { + + {{ 'admin.oauth2.email-attribute-key-required' | translate }} + + } + @if (entityForm.get('mapperConfig.basic.emailAttributeKey').hasError('maxlength')) { + + {{ 'admin.oauth2.email-attribute-key-max-length' | translate }} + + } + +
+ } +
+ + admin.oauth2.first-name-attribute-key + + @if (entityForm.get('mapperConfig.basic.firstNameAttributeKey').hasError('maxlength')) { + + {{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} + + } + + + admin.oauth2.last-name-attribute-key + + @if (entityForm.get('mapperConfig.basic.lastNameAttributeKey').hasError('maxlength')) { + + {{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} + + } + +
+
+ + admin.oauth2.tenant-name-strategy + + @for (tenantNameStrategy of tenantNameStrategies; track tenantNameStrategy) { + + {{ tenantNameStrategy }} + + } + + + + admin.oauth2.tenant-name-pattern + + @if (entityForm.get('mapperConfig.basic.tenantNamePattern').hasError('required')) { + + {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} + + } + @if (entityForm.get('mapperConfig.basic.tenantNamePattern').hasError('maxlength')) { + + {{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} + + } + +
+
+ + admin.oauth2.customer-name-pattern + + @if (entityForm.get('mapperConfig.basic.customerNamePattern').hasError('maxlength')) { + + {{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} + + } + +
+
+ + admin.oauth2.default-dashboard-name + + @if (entityForm.get('mapperConfig.basic.defaultDashboardName').hasError('maxlength')) { + + {{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} + + } + + + {{ 'admin.oauth2.always-fullscreen' | translate}} + +
+
+ } + @if (entityForm.get('mapperConfig.type').value === mapperType.CUSTOM && entityForm.get('mapperConfig.custom')) { +
+
+ + admin.oauth2.url + + @if (entityForm.get('mapperConfig.custom.url').hasError('required')) { + + {{ 'admin.oauth2.url-required' | translate }} + + } + @if (entityForm.get('mapperConfig.custom.url').hasError('pattern')) { + + {{ 'admin.oauth2.url-pattern' | translate }} + + } + @if (entityForm.get('mapperConfig.custom.url').hasError('maxlength')) { + + {{ 'admin.oauth2.url-max-length' | translate }} + + } + +
+
+ + common.username + + @if (entityForm.get('mapperConfig.custom.username').hasError('maxlength')) { + + {{ 'admin.oauth2.username-max-length' | translate }} + + } + + + common.password + + @if (entityForm.get('mapperConfig.custom.password').hasError('maxlength')) { + + {{ 'admin.oauth2.password-max-length' | translate }} + + } + +
+ + {{ 'admin.oauth2.send-token' | translate}} + +
+ }
-
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html index 0816cb6f5a..fb72798997 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html @@ -20,15 +20,21 @@ admin.oauth2.domain-name - - {{ 'admin.oauth2.domain-name-required' | translate }} - - - {{ 'admin.domain-name-max-length' | translate }} - - - {{ 'admin.error-verification-url' | translate }} - + @if (entityForm.get('name').hasError('required')) { + + {{ 'admin.oauth2.domain-name-required' | translate }} + + } + @if (entityForm.get('name').hasError('maxlength')) { + + {{ 'admin.domain-name-max-length' | translate }} + + } + @if (entityForm.get('name').hasError('pattern')) { + + {{ 'admin.error-verification-url' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html index 360b3b50da..1daeab7f89 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html @@ -23,8 +23,10 @@ {{ "javascript.all-types" | translate }} - - {{ resourceSubTypesTranslationMap.get(jsResourceSubType) | translate }} - + @for (jsResourceSubType of jsResourceSubTypes; track jsResourceSubType) { + + {{ resourceSubTypesTranslationMap.get(jsResourceSubType) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html index 901eb78135..78e1ac048b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html @@ -15,9 +15,11 @@ limitations under the License. --> - - - +@if (entity && entity.tenantId.id !== NULL_UUID && authUser.authority === authorities.TENANT_ADMIN && !isEdit) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.html index 1d98f4be18..94ff5f2be5 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.html @@ -15,13 +15,17 @@ limitations under the License. --> - - - - - - +@if (entity && entity.tenantId.id !== NULL_UUID && authUser.authority === authorities.TENANT_ADMIN && !isEdit) { + + + +} +@if (entity && entity.tenantId.id !== NULL_UUID && authUser.authority === authorities.TENANT_ADMIN && !isEdit) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html index 05c6dfed73..5c8d9e4d6b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html @@ -23,8 +23,10 @@ {{ "resource.all-types" | translate }} - - {{ resourceTypesTranslationMap.get(resourceType) | translate }} - + @for (resourceType of resourceTypes; track resourceType) { + + {{ resourceTypesTranslationMap.get(resourceType) | translate }} + + } diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html index 24ee53842d..5ee5643d76 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html @@ -23,9 +23,13 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
@@ -37,17 +41,21 @@ formControlName="maxFailedLoginAttempts" step="1" min="0"/> - - {{ 'admin.minimum-max-failed-login-attempts-range' | translate }} - + @if (securitySettingsFormGroup.get('maxFailedLoginAttempts').hasError('min')) { + + {{ 'admin.minimum-max-failed-login-attempts-range' | translate }} + + } admin.user-lockout-notification-email - - {{ 'login.invalid-email-format' | translate }} - + @if (securitySettingsFormGroup.get('userLockoutNotificationEmail').invalid) { + + {{ 'login.invalid-email-format' | translate }} + + } admin.user-activation-token-ttl @@ -56,26 +64,34 @@ step="1" min="1" max="24"/> - - {{ 'admin.user-activation-token-ttl-range' | translate }} - - - {{ 'admin.user-activation-token-ttl-range' | translate }} - + @if (securitySettingsFormGroup.get('userActivationTokenTtl').hasError('min')) { + + {{ 'admin.user-activation-token-ttl-range' | translate }} + + } + @if (securitySettingsFormGroup.get('userActivationTokenTtl').hasError('max')) { + + {{ 'admin.user-activation-token-ttl-range' | translate }} + + } - + admin.password-reset-token-ttl - - {{ 'admin.password-reset-token-ttl-range' | translate }} - - - {{ 'admin.password-reset-token-ttl-range' | translate }} - + formControlName="passwordResetTokenTtl" + step="1" + min="1" + max="24"/> + @if (securitySettingsFormGroup.get('passwordResetTokenTtl').hasError('min')) { + + {{ 'admin.password-reset-token-ttl-range' | translate }} + + } + @if (securitySettingsFormGroup.get('passwordResetTokenTtl').hasError('max')) { + + {{ 'admin.password-reset-token-ttl-range' | translate }} + + } admin.mobile-secret-key-length @@ -83,9 +99,11 @@ formControlName="mobileSecretKeyLength" step="1" min="1"/> - - {{ 'admin.mobile-secret-key-length-range' | translate }} - + @if (securitySettingsFormGroup.get('mobileSecretKeyLength').hasError('min')) { + + {{ 'admin.mobile-secret-key-length-range' | translate }} + + }
@@ -101,26 +119,36 @@ min="6" max="50" required/> - - {{ 'admin.minimum-password-length-required' | translate }} - - - {{ 'admin.minimum-password-length-range' | translate }} - - - {{ 'admin.minimum-password-length-range' | translate }} - + @if (securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')) { + + {{ 'admin.minimum-password-length-required' | translate }} + + } + @if (securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('min')) { + + {{ 'admin.minimum-password-length-range' | translate }} + + } + @if (securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('max')) { + + {{ 'admin.minimum-password-length-range' | translate }} + + } admin.maximum-password-length - - {{ 'admin.maximum-password-length-min' | translate }} - - - {{ 'admin.maximum-password-length-less-min' | translate }} - + @if (securitySettingsFormGroup.get('passwordPolicy.maximumLength').hasError('min')) { + + {{ 'admin.maximum-password-length-min' | translate }} + + } + @if (securitySettingsFormGroup.get('passwordPolicy.maximumLength').hasError('lessMin')) { + + {{ 'admin.maximum-password-length-less-min' | translate }} + + }
@@ -130,10 +158,12 @@ formControlName="minimumUppercaseLetters" step="1" min="0"/> - - {{ 'admin.minimum-uppercase-letters-range' | translate }} - + @if (securitySettingsFormGroup.get('passwordPolicy.minimumUppercaseLetters').hasError('min')) { + + {{ 'admin.minimum-uppercase-letters-range' | translate }} + + } admin.minimum-lowercase-letters @@ -141,10 +171,12 @@ formControlName="minimumLowercaseLetters" step="1" min="0"/> - - {{ 'admin.minimum-lowercase-letters-range' | translate }} - + @if (securitySettingsFormGroup.get('passwordPolicy.minimumLowercaseLetters').hasError('min')) { + + {{ 'admin.minimum-lowercase-letters-range' | translate }} + + }
@@ -154,9 +186,11 @@ formControlName="minimumDigits" step="1" min="0"/> - - {{ 'admin.minimum-digits-range' | translate }} - + @if (securitySettingsFormGroup.get('passwordPolicy.minimumDigits').hasError('min')) { + + {{ 'admin.minimum-digits-range' | translate }} + + } admin.minimum-special-characters @@ -164,10 +198,12 @@ formControlName="minimumSpecialCharacters" step="1" min="0"/> - - {{ 'admin.minimum-special-characters-range' | translate }} - + @if (securitySettingsFormGroup.get('passwordPolicy.minimumSpecialCharacters').hasError('min')) { + + {{ 'admin.minimum-special-characters-range' | translate }} + + }
@@ -177,10 +213,12 @@ formControlName="passwordExpirationPeriodDays" step="1" min="0"/> - - {{ 'admin.password-expiration-period-days-range' | translate }} - + @if (securitySettingsFormGroup.get('passwordPolicy.passwordExpirationPeriodDays').hasError('min')) { + + {{ 'admin.password-expiration-period-days-range' | translate }} + + } admin.password-reuse-frequency-days @@ -188,10 +226,12 @@ formControlName="passwordReuseFrequencyDays" step="1" min="0"/> - - {{ 'admin.password-reuse-frequency-days-range' | translate }} - + @if (securitySettingsFormGroup.get('passwordPolicy.passwordReuseFrequencyDays').hasError('min')) { + + {{ 'admin.password-reuse-frequency-days-range' | translate }} + + }
@@ -235,9 +275,11 @@ admin.jwt.issuer-name - - {{ 'admin.jwt.issuer-name-required' | translate }} - + @if (jwtSecuritySettingsFormGroup.get('tokenIssuer').hasError('required')) { + + {{ 'admin.jwt.issuer-name-required' | translate }} + + } admin.jwt.signings-key @@ -251,15 +293,21 @@ {{ 'admin.jwt.generate-key' | translate }} admin.jwt.signings-key-hint - - {{ 'admin.jwt.signings-key-required' | translate }} - - - {{ 'admin.jwt.signings-key-base64' | translate }} - - - {{ 'admin.jwt.signings-key-min-length' | translate }} - + @if (jwtSecuritySettingsFormGroup.get('tokenSigningKey').hasError('required')) { + + {{ 'admin.jwt.signings-key-required' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('tokenSigningKey').hasError('base64')) { + + {{ 'admin.jwt.signings-key-base64' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('tokenSigningKey').hasError('minLength')) { + + {{ 'admin.jwt.signings-key-min-length' | translate }} + + }
@@ -270,15 +318,21 @@ step="1" min="60"/> - - {{ 'admin.jwt.expiration-time-required' | translate }} - - - {{ 'admin.jwt.expiration-time-max' | translate }} - - - {{ 'admin.jwt.expiration-time-min' | translate }} - + @if (jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('required')) { + + {{ 'admin.jwt.expiration-time-required' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('max')) { + + {{ 'admin.jwt.expiration-time-max' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('min')) { + + {{ 'admin.jwt.expiration-time-min' | translate }} + + } admin.jwt.refresh-expiration-time @@ -287,18 +341,26 @@ step="1" min="900"/> - - {{ 'admin.jwt.refresh-expiration-time-required' | translate }} - - - {{ 'admin.jwt.refresh-expiration-time-max' | translate }} - - - {{ 'admin.jwt.refresh-expiration-time-min' | translate }} - - - {{ 'admin.jwt.refresh-expiration-time-less-token' | translate }} - + @if (jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('required')) { + + {{ 'admin.jwt.refresh-expiration-time-required' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('max')) { + + {{ 'admin.jwt.refresh-expiration-time-max' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('min')) { + + {{ 'admin.jwt.refresh-expiration-time-min' | translate }} + + } + @if (jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('lessToken')) { + + {{ 'admin.jwt.refresh-expiration-time-less-token' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html b/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html index c36923dc84..b9fddd605d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
admin.sms-message - - {{ 'admin.sms-message-required' | translate }} - - - {{ 'admin.sms-message-max-length' | translate }} - + @if (sendTestSmsFormGroup.get('message').hasError('required')) { + + {{ 'admin.sms-message-required' | translate }} + + } + @if (sendTestSmsFormGroup.get('message').hasError('maxLength')) { + + {{ 'admin.sms-message-max-length' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html index 2c38ad4ab8..a723b89043 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html @@ -15,35 +15,41 @@ limitations under the License. --> - - - - admin.sms-provider-settings - - -
-
- - -
- - - - -
- - -
- -
-
+@if (isSysAdmin()) { + + + + admin.sms-provider-settings + + +
+
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ } + +
+ + +
+ + +
+
+
+
+}
@@ -53,32 +59,40 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ } admin.slack-api-token - - - admin.mobile-settings - - - - - - - - + @if (isSysAdmin()) { + + + admin.mobile-settings + + + + + } + @if (isSysAdmin()) { + + + + + }
diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index 2da7097509..46495abd5e 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -24,9 +24,13 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
@@ -47,42 +51,45 @@ admin.2fa.enforce-for - - {{ notificationTargetConfigTypeInfoMap.get(type).name | translate }} - + @for (type of notificationTargetConfigTypes; track type) { + + {{ notificationTargetConfigTypeInfoMap.get(type).name | translate }} + + } -
-
- - {{ 'tenant.tenant' | translate }} - {{ 'tenant-profile.tenant-profile' | translate }} - -
- - - - - - - - -
+ @if (twoFaFormGroup.get('enforcedUsersFilter.type').value === notificationTargetConfigType.TENANT_ADMINISTRATORS) { +
+
+ + {{ 'tenant.tenant' | translate }} + {{ 'tenant-profile.tenant-profile' | translate }} + +
+ @if (twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').value) { + + + } @else { + + + } +
+ }
@@ -93,42 +100,48 @@ @if (!atListOneProvider) { } - -
- - - - - {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} - - - - - - - - - admin.2fa.issuer-name - - + @for (provider of providersForm.controls; track trackByElement(i, provider); let i = $index) { +
+ + + + + {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} + + + + + @switch (provider.get('providerType').value) { + @case (twoFactorAuthProviderType.TOTP) { + + admin.2fa.issuer-name + + @if (provider.get('issuerName').hasError('required') || + provider.get('issuerName').hasError('pattern')) { + {{ "admin.2fa.issuer-name-required" | translate }} - - -
+ } + + } + @case (twoFactorAuthProviderType.SMS) { +
admin.2fa.verification-message-template - - {{ "admin.2fa.verification-message-template-required" | translate }} - - - {{ "admin.2fa.verification-message-template-pattern" | translate }} - + @if (provider.get('smsVerificationMessageTemplate').hasError('required')) { + + {{ "admin.2fa.verification-message-template-required" | translate }} + + } + @if (provider.get('smsVerificationMessageTemplate').hasError('pattern')) { + + {{ "admin.2fa.verification-message-template-pattern" | translate }} + + }
-
+ } + @case (twoFactorAuthProviderType.EMAIL) { +
-
+ } + @case (twoFactorAuthProviderType.BACKUP_CODE) { +
admin.2fa.number-of-codes - - {{ "admin.2fa.number-of-codes-required" | translate }} - - - {{ "admin.2fa.number-of-codes-pattern" | translate }} - + @if (provider.get('codesQuantity').hasError('required')) { + + {{ "admin.2fa.number-of-codes-required" | translate }} + + } + @if (provider.get('codesQuantity').hasError('min') || + provider.get('codesQuantity').hasError('pattern')) { + + {{ "admin.2fa.number-of-codes-pattern" | translate }} + + }
- - - -
- + } + } + + +
+ }
admin.2fa.verification-limitations
@@ -197,11 +219,13 @@ admin.2fa.max-verification-failures-before-user-lockout - - {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} - + || twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('max')) { + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} + + }
@@ -219,13 +243,17 @@ admin.2fa.number-of-checking-attempts - - {{ 'admin.2fa.number-of-checking-attempts-required' | translate }} - - - {{ 'admin.2fa.number-of-checking-attempts-pattern' | translate }} - + @if (twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('required')) { + + {{ 'admin.2fa.number-of-checking-attempts-required' | translate }} + + } + @if (twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('pattern') + || twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('min')) { + + {{ 'admin.2fa.number-of-checking-attempts-pattern' | translate }} + + } - - - - - - +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html index 304ea22c33..0cebbc0286 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html @@ -25,19 +25,23 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
- - + +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html index 43f0f3dc6b..518115993a 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html @@ -25,13 +25,15 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
- - +
{{ publicLink }}
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
{{ labelText | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html index 69dd15b60f..db5287fd68 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -17,28 +17,36 @@ --> @if (entity) { -
- - device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} - - - - {{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} - - -
- - + @if (transportType.isActive) { +
+ + device-profile.transport-type + + @for (type of deviceTransportTypes; track type) { + + {{deviceTransportTypeTranslations.get(type) | translate}} + + } + + @if (detailsForm.get('transportType').value) { + + {{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}} + + } + @if (detailsForm.get('transportType').hasError('required')) { + + {{ 'device-profile.transport-type-required' | translate }} + + } + +
+ + +
-
+ } @if (authUser.authority === authorities.TENANT_ADMIN && !isEdit) { @@ -58,11 +66,13 @@
} @if (alarmRulesOldVersion || isEdit) { -
-
- + @if (alarmRules.isActive) { +
+
+ +
-
+ } } @else {
-
-
- - + @if (deviceProvisioning.isActive) { +
+
+ + +
-
+ } @if (!isEdit) { @@ -101,13 +113,15 @@ } } - -
-
- - +@if (false) { + +
+
+ + +
-
- + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html index eaa5d38f0b..64d28b2b9a 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html @@ -16,12 +16,14 @@ -->
-
- - - - +
+ @switch (type) { + @case (deviceProfileType.DEFAULT) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html index d5690a9cdc..cb9b8eed57 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html @@ -17,27 +17,31 @@ -->
- - - -
device.device-configuration
-
-
- - -
- - - -
device.transport-configuration
-
-
- - -
+ @if (displayDeviceConfiguration) { + + + +
device.device-configuration
+
+
+ + +
+ } + @if (displayTransportConfiguration) { + + + +
device.transport-configuration
+
+
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html index b18974839e..41f2007ce9 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html @@ -16,36 +16,38 @@ -->
-
- - - - - - - - - - - - - - - - - - - - +
+ @switch (transportType) { + @case (deviceTransportType.DEFAULT) { + + + } + @case (deviceTransportType.MQTT) { + + + } + @case (deviceTransportType.LWM2M) { + + + } + @case (deviceTransportType.COAP) { + + + } + @case (deviceTransportType.SNMP) { + + + } + }
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html index 631c28ad98..992a88ab16 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html @@ -20,114 +20,146 @@ device-profile.snmp.host - - {{ 'device-profile.snmp.host-required' | translate }} - + @if (snmpDeviceTransportConfigurationFormGroup.get('host').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('host').hasError('pattern')) { + + {{ 'device-profile.snmp.host-required' | translate }} + + } device-profile.snmp.port - - {{ 'device-profile.snmp.port-required' | translate }} - - - {{ 'device-profile.snmp.port-format' | translate }} - + @if (snmpDeviceTransportConfigurationFormGroup.get('port').hasError('required')) { + + {{ 'device-profile.snmp.port-required' | translate }} + + } + @if (snmpDeviceTransportConfigurationFormGroup.get('port').hasError('pattern') || + snmpDeviceTransportConfigurationFormGroup.get('port').hasError('min')) { + + {{ 'device-profile.snmp.port-format' | translate }} + + }
device-profile.snmp.protocol-version - - {{ snmpDeviceProtocolVersion | lowercase }} - + @for (snmpDeviceProtocolVersion of snmpDeviceProtocolVersions; track snmpDeviceProtocolVersion) { + + {{ snmpDeviceProtocolVersion | lowercase }} + + } - - {{ 'device-profile.snmp.protocol-version-required' | translate }} - - -
- - device-profile.snmp.community - - - {{ 'device-profile.snmp.community-required' | translate }} + @if (snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').hasError('required')) { + + {{ 'device-profile.snmp.protocol-version-required' | translate }} - -
-
-
- - device-profile.snmp.user-name - - - {{ 'device-profile.snmp.user-name-required' | translate }} - - - - device-profile.snmp.security-name - - - {{ 'device-profile.snmp.security-name-required' | translate }} - - -
-
- - device-profile.snmp.authentication-protocol - - - {{ snmpAuthenticationProtocolTranslation.get(snmpAuthenticationProtocol) }} - - - - {{ 'device-profile.snmp.authentication-protocol-required' | translate }} - - - - device-profile.snmp.authentication-passphrase - - - {{ 'device-profile.snmp.authentication-passphrase-required' | translate }} - - -
-
- - device-profile.snmp.privacy-protocol - - - {{ snmpPrivacyProtocolTranslation.get(snmpPrivacyProtocol) }} - - - - {{ 'device-profile.snmp.privacy-protocol-required' | translate }} - - - - device-profile.snmp.privacy-passphrase - - - {{ 'device-profile.snmp.privacy-passphrase-required' | translate }} - - -
-
- - device-profile.snmp.context-name - - - - device-profile.snmp.engine-id - + } + + @if (!isV3protocolVersion()) { +
+ + device-profile.snmp.community + + @if (snmpDeviceTransportConfigurationFormGroup.get('community').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('community').hasError('pattern')) { + + {{ 'device-profile.snmp.community-required' | translate }} + + } -
-
+ + } + @if (isV3protocolVersion()) { +
+
+ + device-profile.snmp.user-name + + @if (snmpDeviceTransportConfigurationFormGroup.get('username').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('username').hasError('pattern')) { + + {{ 'device-profile.snmp.user-name-required' | translate }} + + } + + + device-profile.snmp.security-name + + @if (snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('pattern')) { + + {{ 'device-profile.snmp.security-name-required' | translate }} + + } + +
+
+ + device-profile.snmp.authentication-protocol + + @for (snmpAuthenticationProtocol of snmpAuthenticationProtocols; track snmpAuthenticationProtocol) { + + {{ snmpAuthenticationProtocolTranslation.get(snmpAuthenticationProtocol) }} + + } + + @if (snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').hasError('required')) { + + {{ 'device-profile.snmp.authentication-protocol-required' | translate }} + + } + + + device-profile.snmp.authentication-passphrase + + @if (snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('pattern')) { + + {{ 'device-profile.snmp.authentication-passphrase-required' | translate }} + + } + +
+
+ + device-profile.snmp.privacy-protocol + + @for (snmpPrivacyProtocol of snmpPrivacyProtocols; track snmpPrivacyProtocol) { + + {{ snmpPrivacyProtocolTranslation.get(snmpPrivacyProtocol) }} + + } + + @if (snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').hasError('required')) { + + {{ 'device-profile.snmp.privacy-protocol-required' | translate }} + + } + + + device-profile.snmp.privacy-passphrase + + @if (snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('required') || + snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('pattern')) { + + {{ 'device-profile.snmp.privacy-passphrase-required' | translate }} + + } + +
+
+ + device-profile.snmp.context-name + + + + device-profile.snmp.engine-id + + +
+
+ } diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html index fd30184330..20ef481b4a 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html @@ -18,7 +18,7 @@

{{ dialogTitle }}

- +
-
- - - {{ deviceTransportTypeTranslationMap.get(BasicTransportType.HTTP) | translate }} - - - {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.MQTT) | translate }} - - - {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.COAP) | translate }} - - - {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.SNMP) | translate }} - - - {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.LWM2M) | translate }} - - -
- - -
device.connectivity.use-following-instructions
- - - - - Windows - - -
-
-
device.connectivity.install-necessary-client-tools
-
device.connectivity.install-curl-windows
-
- - -
-
-
- - - - MacOS - - -
-
-
device.connectivity.install-necessary-client-tools
-
device.connectivity.install-curl-macos
-
- - -
-
-
- - - - Linux - - -
-
-
device.connectivity.install-necessary-client-tools
- -
- - -
-
-
-
-
- -
- - -
- + @if (loadedCommand) { +
+ + @if (allowTransportType.has(BasicTransportType.HTTP)) { + + {{ deviceTransportTypeTranslationMap.get(BasicTransportType.HTTP) | translate }} + + } + @if (allowTransportType.has(DeviceTransportType.MQTT)) { + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.MQTT) | translate }} + + } + @if (allowTransportType.has(DeviceTransportType.COAP)) { + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.COAP) | translate }} + + } + @if (allowTransportType.has(DeviceTransportType.SNMP)) { + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.SNMP) | translate }} + + } + @if (allowTransportType.has(DeviceTransportType.LWM2M)) { + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.LWM2M) | translate }} + + } + +
+ @switch (transportProtocol.value) { + @case (BasicTransportType.HTTP) {
device.connectivity.use-following-instructions
- + @@ -146,18 +79,13 @@
device.connectivity.install-necessary-client-tools
-
- - -
+
device.connectivity.install-curl-windows
+ *ngTemplateOutlet="executeCommand; context: {cmd: { + noSecLabel: 'HTTP', noSec: commands.http.http, + secLabel: 'HTTPs', sec: commands.http.https, + doc: {text: '', href: ''}}}">
@@ -171,14 +99,13 @@
device.connectivity.install-necessary-client-tools
- +
device.connectivity.install-curl-macos
+ *ngTemplateOutlet="executeCommand; context: {cmd: { + noSecLabel: 'HTTP', noSec: commands.http.http, + secLabel: 'HTTPs', sec: commands.http.https, + doc: {text: '', href: ''}}}">
@@ -193,146 +120,235 @@
device.connectivity.install-necessary-client-tools
+ [data]='createMarkDownCommand("sudo apt-get install curl")'>
+ *ngTemplateOutlet="executeCommand; context: {cmd: { + noSecLabel: 'HTTP', noSec: commands.http.http, + secLabel: 'HTTPs', sec: commands.http.https, + doc: {text: '', href: ''}}}">
- - - - Docker - - -
- + } + @case (DeviceTransportType.MQTT) { + @if (commands.mqtt.sparkplug) { +
+ + +
+ } @else { +
device.connectivity.use-following-instructions
+ + + + + Windows + + +
+
+
device.connectivity.install-necessary-client-tools
+
+ + +
+
+ + +
+
+
+ + + + MacOS + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+ + + + Linux + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+ @if (!!commands.mqtt.docker) { + + + + Docker + + +
+ - -
+
+
+
+
+ } + + } + } + @case (DeviceTransportType.COAP) { +
device.connectivity.use-following-instructions
+ + + + + Linux - - - - - -
device.connectivity.use-following-instructions
- - - - - Linux - - -
-
-
device.connectivity.install-necessary-client-tools
-
+ +
+ @if (commands.coap.coaps !== 'Check documentation') { +
+
device.connectivity.install-necessary-client-tools
+
- -
-
- + +
+
+ } + - -
- - - - - - Docker - - -
- +
+
+
+ @if (!!commands.coap.docker) { + + + + Docker + + +
+ - -
-
-
- - - -
- +
+
+ + } + + } + @case (DeviceTransportType.SNMP) { +
+ - -
- - -
- +
+ } + @case (DeviceTransportType.LWM2M) { +
+ - -
-
- -
-
-
-
device.state
-
- {{ (status ? 'device.active' : 'device.inactive') | translate }} -
+ +
+ } + }
-
attribute.latest-telemetry
-
-
-
device.time
-
attribute.key
-
attribute.value
+
+
+
device.state
+
+ {{ (status ? 'device.active' : 'device.inactive') | translate }} +
-
-
-
{{ telemetry.lastUpdateTs | date: 'yyyy-MM-dd HH:mm:ss' }}
-
{{ telemetry.key }}
-
{{ telemetry.value }}
+
attribute.latest-telemetry
+
+
+
device.time
+
attribute.key
+
attribute.value
+ @if (latestTelemetry.length) { +
+ @for (telemetry of latestTelemetry; track telemetry) { +
+
{{ telemetry.lastUpdateTs | date: 'yyyy-MM-dd HH:mm:ss' }}
+
{{ telemetry.key }}
+
{{ telemetry.value }}
+
+ } +
+ } @else { +
+
+
attribute.no-latest-telemetry
+
+ }
+
+ } @else { +
+ + + {{ 'device.connectivity.loading-check-connectivity-command' | translate }} +
-
+ }
{{ 'action.dont-show-again' | translate}} + (click)="close()">{{ 'action.close' | translate }}
- -
- - - {{ 'device.connectivity.loading-check-connectivity-command' | translate }} - -
-
- -
-
-
attribute.no-latest-telemetry
-
-
@@ -344,24 +360,25 @@ {{ cmd.secLabel }}
- - + @switch (protocolType.value) { + @case ('noSec') { - - -
- -
- + [data]=createMarkDownCommand(cmd.noSec)> + } + @case ('sec') { + @if (cmd.sec !== 'Check documentation') { +
+ +
+ } @else { -
-
-
+ } + } + }
diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html index b0f26e1ccf..5f679a5389 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html @@ -24,25 +24,28 @@ close - - +@if ((isLoading$ | async)) { + + +}
-
-
- - -
-
- + @if (!loadingCredentials) { +
+
+ + +
+
+ } @else {
{{ 'device.loading-device-credentials' | translate }}
-
+ }
- + {{ 'action.save' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html index ed523cf98d..9376ff1b3e 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html @@ -25,64 +25,65 @@
-
-
- - - - - Docker - - -
-
- + @if (loadedInstructions) { +
+
+ + + + + Docker + + +
+
+ +
-
- - - - - - Ubuntu - - -
-
- + + + + + + Ubuntu + + +
+
+ +
-
- - - - - - CentOS/RHEL - - -
-
- + + + + + + CentOS/RHEL + + +
+
+ +
-
- - - + + + +
+
+ } @else { +
+ + + {{ 'edge.loading-edge-instructions' | translate }} +
-
+ }
{{ 'action.dont-show-again' | translate}} + (click)="close()">{{ 'action.close' | translate }}
- -
- - - {{ 'edge.loading-edge-instructions' | translate }} - -
-
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html index b912757027..3f298aff3d 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-tabs.component.html @@ -15,42 +15,56 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + +} +@if (entity) { + + + + +} +@if (entity) { + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html index 604d6469f9..2d2eaa32c9 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html @@ -15,38 +15,50 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + +} +@if (entity) { + + + + +} +@if (entity) { + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html index d3269fae53..848430c0cf 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html @@ -15,25 +15,30 @@ limitations under the License. --> - - +@if (homeDashboard) { + +} @else { - - - - {{section.name}} - - - - - - {{place.icon}} - {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} - - - - - - + @for (section of homeSections$| async; track section) { + + + + {{section.name}} + + + + @for (place of section.places; track place) { + + + {{place.icon}} + {{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}} + + + } + + + + + } - +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html index d984ac99c9..ab4417c23b 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html @@ -26,9 +26,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html index bf8bbacc83..0e88ff03a6 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html @@ -28,30 +28,40 @@ icon="content_copy"> - - {{ 'mobile.mobile-package-required' | translate }} - - - {{ 'mobile.mobile-package-max-length' | translate }} - - - {{ 'mobile.mobile-package-pattern' | translate }} - + @if (entityForm.get('pkgName').hasError('required')) { + + {{ 'mobile.mobile-package-required' | translate }} + + } + @if (entityForm.get('pkgName').hasError('maxlength')) { + + {{ 'mobile.mobile-package-max-length' | translate }} + + } + @if (entityForm.get('pkgName').hasError('pattern')) { + + {{ 'mobile.mobile-package-pattern' | translate }} + + } mobile.mobile-package-title - - {{ 'mobile.mobile-package-title-max-length' | translate }} - + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'mobile.mobile-package-title-max-length' | translate }} + + } mobile.platform-type - - {{ platformTypeTranslations.get(platformType) | translate }} - + @for (platformType of platformTypes; track platformType) { + + {{ platformTypeTranslations.get(platformType) | translate }} + + } @@ -59,14 +69,15 @@
- + @if (isEdit) { + + }
- - {{ 'mobile.application-secret-required' | translate }} - + @if (entityForm.get('appSecret').hasError('required')) { + + {{ 'mobile.application-secret-required' | translate }} + + }
mobile.status - - {{ mobileAppStatusTranslations.get(mobileAppStatus) | translate }} - + @for (mobileAppStatus of mobileAppStatuses; track mobileAppStatus) { + + {{ mobileAppStatusTranslations.get(mobileAppStatus) | translate }} + + }
@@ -96,9 +111,11 @@ mobile.min-version - - {{ 'mobile.invalid-version-pattern' | translate }} - + @if (entityForm.get('versionInfo.minVersion').hasError('pattern')) { + + {{ 'mobile.invalid-version-pattern' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html index d9aa1ada3d..c35758d385 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html @@ -23,9 +23,13 @@ close - - -
+@if (isLoading$ | async) { + + +} +@if (!(isLoading$ | async)) { +
+}
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html index 95b34f5866..e422703215 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html @@ -30,41 +30,46 @@ mobile.page-name - - warning - - - warning - - - warning - + @if (customMobilePageForm.get('label').hasError('required') && customMobilePageForm.get('label').touched) { + + warning + + } + @if (customMobilePageForm.get('label').hasError('pattern') && customMobilePageForm.get('label').touched) { + + warning + + } + @if (customMobilePageForm.get('label').hasError('maxlength') && customMobilePageForm.get('label').touched) { + + warning + + }
mobile.page-type - - {{ mobilePageTypeTranslations.get(pageType) | translate }} - + @for (pageType of mobilePagesTypes; track pageType) { + + {{ mobilePageTypeTranslations.get(pageType) | translate }} + + } - + @if (customMobilePageForm.get('type').value === MobilePageType.DASHBOARD) { - - + } + @if (customMobilePageForm.get('type').value === MobilePageType.WEB_VIEW) { mobile.url - - {{ 'mobile.invalid-url-format' | translate }} - + @if (customMobilePageForm.get('url').hasError('pattern')) { + + {{ 'mobile.invalid-url-format' | translate }} + + } - - + } + @if (customMobilePageForm.get('type').value === MobilePageType.CUSTOM) { mobile.path - - {{ 'mobile.invalid-path-format' | translate }} - + @if (customMobilePageForm.get('path').hasError('pattern')) { + + {{ 'mobile.invalid-path-format' | translate }} + + } - + } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html index 14f54ae664..b33aa51ef1 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/default-mobile-page-panel.component.html @@ -18,16 +18,18 @@
mobile.edit-page
- + @if (!disabled) { + + }
@@ -43,22 +45,24 @@ mobile.page-name - - warning - - - warning - + @if (mobilePageFormGroup.get('label').hasError('pattern') && mobilePageFormGroup.get('label').touched) { + + warning + + } + @if (mobilePageFormGroup.get('label').hasError('maxlength') && mobilePageFormGroup.get('label').touched) { + + warning + + }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.html b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.html index f7ab3fe2b3..24bde59818 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.html @@ -25,13 +25,14 @@ (click)="cancel()"> {{ 'action.cancel' | translate }} - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html index 76d088a20c..83419e2191 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html @@ -22,9 +22,13 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
@@ -35,18 +39,20 @@ {{ 'admin.mobile-app.custom' | translate }}
-
-
{{ 'mobile.bundle' | translate }}
- - -
+ @if (!mobileAppSettingsForm.get('useDefaultApp').value) { +
+
{{ 'mobile.bundle' | translate }}
+ + +
+ }
{{ 'admin.mobile-app.android' | translate }} @@ -66,48 +72,57 @@ {{ 'admin.mobile-app.disabled' | translate }}
-
-
- - {{ 'admin.mobile-app.badges' | translate }} - - - - - {{ badgePosition.value | translate }} - - - + @if (mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { +
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + @for (badgePosition of badgePositionTranslationsMap | keyvalue; track badgePosition) { + + {{ badgePosition.value | translate }} + + } + + +
-
-
-
- - {{ 'admin.mobile-app.label' | translate }} - - - - +
+ + {{ 'admin.mobile-app.label' | translate }} + + + + @if (!mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').untouched && + mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('required') || + mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('maxlength')) { + - warning - - + class="tb-error"> + warning + + } + +
-
-
-
admin.mobile-app.preview
- - -
+ } + @if (mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { +
+
admin.mobile-app.preview
+ + +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html index 16430dc22a..53f554e706 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html @@ -25,16 +25,22 @@
-
-
{{ NotificationDeliveryMethodInfoMap.get(errorStat.key).name | translate }}
- - - - - -
- {{ error.key }} - {{ error.value }}
- -
+ @for (errorStat of errorStatEntries; track errorStat; let isLast = $last) { +
+
{{ NotificationDeliveryMethodInfoMap.get(errorStat.key).name | translate }}
+ + @for (error of errorStatEntries; track error) { + + + + + } +
+ {{ error.key }} + {{ error.value }}
+ @if (!isLast) { + + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts index ea6e0bed5c..a81bce6eae 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts @@ -39,6 +39,7 @@ export interface NotificationRequestErrorDialogData { export class SentErrorDialogComponent extends DialogComponent { errorStats: { [key in NotificationDeliveryMethod]: {[errorKey in string]: string}}; + errorStatEntries: Array<{ key: NotificationDeliveryMethod; value: { [k: string]: string } }>; NotificationDeliveryMethodInfoMap = NotificationDeliveryMethodInfoMap; @@ -49,7 +50,7 @@ export class SentErrorDialogComponent extends DialogComponent).map(([key, value]) => ({ key, value })); } cancel(): void { this.dialogRef.close(null); diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 3d159c14b2..1a0be821be 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -25,8 +25,10 @@ close - - +@if (isLoading$ | async) { + + +}
-
-
-
- -
notification.at-least-one-should-be-selected
+
+
+
+ +
notification.at-least-one-should-be-selected
+
+ @if (showRefresh) { + + }
- -
-
- - - - {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} - - - chevron_right - - - -
- - {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} - -
-
-
+
+ @for (deliveryMethods of notificationDeliveryMethods; track deliveryMethods) { + @if (isInteractDeliveryMethod(deliveryMethods)) { + + + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} + + @if (isInteractDeliveryMethod(deliveryMethods)) { + + chevron_right + + } + + } @else { +
+ + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} + +
+ } + } +
-
}
@@ -279,5 +285,5 @@ + (click)="nextStep()">{{ nextStepLabel() | translate }}
diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html index 856cf091a6..d4dda48d4c 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html @@ -29,13 +29,15 @@
-
-
- + @for (deliveryMethod of deliveryMethods; track deliveryMethod) { +
+
+ +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html index 608a11539e..73d0209640 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html @@ -25,13 +25,17 @@
+ (click)="resetSettings()">{{ 'notification.settings.reset-all' | translate }}
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
@@ -46,25 +50,31 @@ notification.settings.type
-
-
- - {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} - + @for (deliveryMethods of notificationDeliveryMethods; track deliveryMethods) { +
+
+ + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }} + +
-
+ }
-
- - - -
+ @for (settingsControl of notificationSettingsFormArray.controls; track settingsControl; let i = $index) { +
+ + + @if (!$last) { + + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html index c94f7eaca7..61e0f138da 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html @@ -15,9 +15,11 @@ limitations under the License. --> - - - +@if (isTenantOtaUpdate() && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html index 774edbd184..697dcf86af 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -29,21 +29,29 @@
- - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
user.email - - {{ 'user.email-required' | translate }} - - - {{ 'user.invalid-email-format' | translate }} - + @if (profile.get('email').hasError('required')) { + + {{ 'user.email-required' | translate }} + + } + @if (profile.get('email').hasError('email')) { + + {{ 'user.invalid-email-format' | translate }} + + } user.first-name @@ -78,20 +86,22 @@ } -
- - - {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} - -
+ @if (!isSysAdmin()) { +
+ + + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} + +
+ }
- - + @if (isLoading$ | async) { + + + }
close - - + @if (isLoading$ | async) { + + + }
close - - + @if (isLoading$ | async) { + + + }
rulechain.name - - {{ 'rulechain.name-required' | translate }} - - - {{ 'rulechain.name-max-length' | translate }} - + @if (createNestedRuleChainFormGroup.get('name').hasError('required')) { + + {{ 'rulechain.name-required' | translate }} + + } + @if (createNestedRuleChainFormGroup.get('name').hasError('maxlength')) { + + {{ 'rulechain.name-max-length' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html index 65aabc17aa..d243baaa29 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html @@ -18,13 +18,16 @@ rulenode.link-labels - - {{label.name}} - close - + @for (label of labels; track label) { + + {{label.name}} + @if (!disabled) { + close + } + + } - - - - -
-
- rulenode.no-link-labels-found + @for (label of filteredLabels | async; track label) { + + + + } + @if ((filteredLabels | async)?.length === 0) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ rulenode.no-link-labels-found +
+ } @else { + + {{ translate.get('rulenode.no-link-label-matching', + {label: truncate.transform(searchText, true, 6, '...')}) | async }} + + } + @if (allowCustom) { + + rulenode.create-new-link-label + + }
- - - {{ translate.get('rulenode.no-link-label-matching', - {label: truncate.transform(searchText, true, 6, '...')}) | async }} - - - - rulenode.create-new-link-label - -
- + + } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html index 25d2fd315d..f1108b17ea 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html @@ -17,12 +17,16 @@ -->
-
{{definedDirectiveError}}
- - + @if (definedDirectiveError) { +
{{definedDirectiveError}}
+ } + @if (!useDefinedDirective()) { + + + }
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 daf8fce8e5..a59ad5c547 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 @@ -15,12 +15,14 @@ limitations under the License. --> -
- -
+@if (!isAdd && ruleNode.component?.configurationDescriptor?.nodeDefinition?.ruleChainNode) { +
+ +
+}
@@ -43,7 +45,7 @@ class="mr-2" formControlName="debugSettings" [entityType]="entityType.RULE_NODE" - /> + /> @if (isSingleton()) { - + @if (!isImport) { + + } -
+ @for (menuItem of contextInfo.menuItems; track menuItem) { +
+ @if (menuItem.divider) { + + } + @if (!menuItem.divider) { + + } +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html index cd93367fc0..ecf2433b31 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html @@ -15,46 +15,60 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + +} +@if (entity) { + + + + +} +@if (entity) { + + + +} +@if (entity) { + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html index e406168acb..c125d1ab1b 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html @@ -16,52 +16,66 @@ -->
+ (dblclick)="userNodeCallbacks.doubleClick($event, node)" + (mousedown)="userNodeCallbacks.mouseDown($event, node)" + (mouseenter)="userNodeCallbacks.mouseEnter($event, node)" + (mouseleave)="userNodeCallbacks.mouseLeave($event, node)">
- {{node.icon}} - + @if (!iconUrl) { + {{node.icon}} + } + @if (iconUrl) { + + }
{{ node.component.name }} - {{ node.name }} + @if (node.name) { + {{ node.name }} + }
-
-
-
+ @for (connector of modelservice.nodes.getConnectorsByType(node, flowchartConstants.leftConnectorType); track connector) { +
+
+
+ }
-
-
-
+ @for (connector of modelservice.nodes.getConnectorsByType(node, flowchartConstants.rightConnectorType); track connector) { +
+
+
+ }
-
-
- -
-
- × -
-
-
-
- -
-
+ @if (modelservice.isEditable() && !node.readonly) { +
+
+ +
+
+ × +
+
+ } + @if (node.component?.configurationDescriptor?.nodeDefinition?.ruleChainNode && modelservice.isEditable()) { +
+
+ +
+
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html index 3275e3e898..81eef74152 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html @@ -46,31 +46,39 @@
scada.behavior.type
- - {{ scadaSymbolBehaviorTypeTranslations.get(type) | translate }} - + @for (type of scadaSymbolBehaviorTypes; track type) { + + {{ scadaSymbolBehaviorTypeTranslations.get(type) | translate }} + + }
-
-
scada.behavior.value-type
- - - -
- - {{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }} -
-
- - - {{ valueTypesMap.get(valueType).name | translate }} - -
-
-
- - + @if ([ScadaSymbolBehaviorType.value, ScadaSymbolBehaviorType.action].includes(behaviorFormGroup.get('type').value)) { +
+
scada.behavior.value-type
+ + + @if (behaviorFormGroup.get('valueType').value) { + +
+ + {{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }} +
+
+ } + @for (valueType of valueTypes; track valueType) { + + + {{ valueTypesMap.get(valueType).name | translate }} + + } +
+
+
+ } + @if (behaviorFormGroup.get('type').value === ScadaSymbolBehaviorType.value) { + @if (behaviorFormGroup.get('valueType').value === ValueType.BOOLEAN) {
scada.behavior.true-label
@@ -89,7 +97,7 @@
-
+ }
scada.behavior.default-settings
-
- + } + @if (behaviorFormGroup.get('type').value === ScadaSymbolBehaviorType.action) {
scada.behavior.default-settings
-
- + } + @if (behaviorFormGroup.get('type').value === ScadaSymbolBehaviorType.widgetAction) {
scada.behavior.default-settings
-
+ }
- + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html index 195d2de0fd..819bb8de99 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html @@ -24,9 +24,11 @@ - - {{ scadaSymbolBehaviorTypeTranslations.get(type) | translate }} - + @for (type of scadaSymbolBehaviorTypes; track type) { + + {{ scadaSymbolBehaviorTypeTranslations.get(type) | translate }} + + }
@@ -38,13 +40,15 @@ matTooltipPosition="above"> settings - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html index c65ea45ebb..f85a37dee9 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html @@ -23,43 +23,51 @@
scada.behavior.type
-
-
- - -
- -
+ @if (behaviorsFormArray().controls.length) { +
+ @for (behaviorControl of behaviorsFormArray().controls; track trackByBehavior($index, behaviorControl)) { +
+ + +
+ @if (!disabled) { + + } +
+
+ }
-
- -
-
- + } @else { + {{ 'scada.behavior.no-behaviors' | translate }} + } + @if (errorText) { + + }
+ @if (!disabled) { +
+ +
+ }
- - {{ 'scada.behavior.no-behaviors' | translate }} - diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html index a7fbbfce2e..e7782cebb9 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html @@ -19,22 +19,22 @@
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html index 643e161e37..b3a64d6c0e 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html @@ -26,23 +26,27 @@
- + @if (!disabled || tagFormGroup.get('stateRenderFunction').value) { + + }
- + @if (!disabled || tagFormGroup.get('clickAction').value) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html index 225596a062..38d1532b0f 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html @@ -22,18 +22,23 @@
scada.state-render-function
scada.tag.on-click-action
-
-
- - - + @if (tagsFormArray().controls.length) { +
+ @for (tagControl of tagsFormArray().controls; track trackByTag($index, tagControl)) { +
+ + + @if (!$last) { + + } +
+ }
-
+ } @else { + {{ 'scada.tag.no-tags' | translate }} + }
- - {{ 'scada.tag.no-tags' | translate }} - diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html index bba6f4319e..9dfcd7bd24 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html @@ -43,14 +43,16 @@ remove
-
- -
+ @if (displayShowHidden) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts index 60baf0e63b..f4065f8f93 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -184,30 +184,36 @@ class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implem @Component({ template: `
- {{ symbolElement?.element?.type }}{{ symbolElement?.invisible ? ' (' + ('scada.hidden' | translate) + ')' : '' }}: - {{ symbolElement?.tag }} - - - -
`, + {{ symbolElement?.element?.type }}{{ symbolElement?.invisible ? ' (' + ('scada.hidden' | translate) + ')' : '' }}: + {{ symbolElement?.tag }} + @if (!symbolElement?.readonly) { + + } + @if (displayTagSettings) { + + } + @if (!symbolElement?.readonly) { + + } +
`, styleUrls: ['./scada-symbol-tooltip.component.scss'], encapsulation: ViewEncapsulation.None, standalone: false @@ -360,37 +366,49 @@ class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent imp @Component({ template: `
-
scada.state-render-function
- - -
scada.tag.on-click-action
- - -
`, + @if (!symbolElement?.readonly || hasStateRenderFunction) { +
scada.state-render-function
+ } + @if (hasStateRenderFunction) { + + } + @if (!hasStateRenderFunction && !symbolElement?.readonly) { + + } + @if (!symbolElement?.readonly || hasClickAction) { +
scada.tag.on-click-action
+ } + @if (hasClickAction) { + + } + @if (!hasClickAction && !symbolElement?.readonly) { + + } +
`, styleUrls: ['./scada-symbol-tooltip.component.scss'], encapsulation: ViewEncapsulation.None, standalone: false diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html index 5b8abc406e..06d8ff153b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -37,46 +37,48 @@ {{ 'scada.create-widget' | translate }}
-
-
- - - - -
-
-
-
-
scada.symbol.symbol
- - + @if (previewMode) { +
+
+ + + + +
+
+
+
+
scada.symbol.symbol
+ + +
+ +
- -
-
+ }
visibility {{ 'scada.preview' | translate }} -
- - -
+ @if (!readonly) { +
+ + +
+ }
- - - + @if (!previewMode) { + + } + @if (previewMode) { + + + }
diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html index 3822fea0e1..db8e6b1cae 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html @@ -24,15 +24,21 @@ close - - -
+@if (isLoading$ | async) { + + +} +@if (!(isLoading$ | async)) { +
+}

security.2fa.dialog.backup-code-description

-
- {{ code }} -
+ @for (code of backupCode?.codes; track code) { +
+ {{ code }} +
+ }
- - -
+@if (isLoading$ | async) { + + +} +@if (!(isLoading$ | async)) { +
+}
@@ -41,12 +45,16 @@ - - {{ 'user.email-required' | translate }} - - - {{ 'user.invalid-email-format' | translate }} - + @if (emailConfigForm.get('email').hasError('required')) { + + {{ 'user.email-required' | translate }} + + } + @if (emailConfigForm.get('email').hasError('email')) { + + {{ 'user.invalid-email-format' | translate }} + + } - - -
+@if (isLoading$ | async) { + + +} +@if (!(isLoading$ | async)) { +
+}
@@ -64,9 +68,11 @@ inputmode="numeric" pattern="[0-9]*" autocomplete="off" placeholder="{{ 'security.2fa.dialog.verification-code' | translate }}"> - - {{ 'security.2fa.dialog.verification-code-invalid' | translate }} - + @if (smsVerificationForm.get('verificationCode').invalid) { + + {{ 'security.2fa.dialog.verification-code-invalid' | translate }} + + }
- - -
+@if (isLoading$ | async) { + + +} +@if (!(isLoading$ | async)) { +
+}
@@ -72,9 +76,11 @@ inputmode="numeric" pattern="[0-9]*" autocomplete="off" placeholder="{{ 'security.2fa.dialog.verification-code' | translate }}"> - - {{ 'security.2fa.dialog.verification-code-invalid' | translate }} - + @if (totpConfigForm.get('verificationCode').invalid) { + + {{ 'security.2fa.dialog.verification-code-invalid' | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.html b/ui-ngx/src/app/modules/home/pages/security/security.component.html index 4fca803187..2488f1846c 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.html @@ -54,30 +54,40 @@ profile.current-password - - {{ 'security.password-requirement.incorrect-password-try-again' | translate }} - + @if (changePassword.get('currentPassword').hasError('differencePassword')) { + + {{ 'security.password-requirement.incorrect-password-try-again' | translate }} + + } login.new-password - - {{ 'security.password-requirement.password-not-meet-requirements' | translate }} - - - {{ changePassword.get('newPassword').getError('alreadyUsed') }} - - - {{ 'security.password-requirement.password-should-difference' | translate }} - - - {{ 'security.password-requirement.password-should-not-contain-spaces' | translate }} - + @if (changePassword.get('newPassword').errors + && !changePassword.get('newPassword').hasError('alreadyUsed') + && !changePassword.get('newPassword').hasError('hasWhitespaces') + && !changePassword.get('newPassword').hasError('passwordSameAsOld')) { + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + } + @if (changePassword.get('newPassword').hasError('alreadyUsed')) { + + {{ changePassword.get('newPassword').getError('alreadyUsed') }} + + } + @if (changePassword.get('newPassword').hasError('passwordSameAsOld')) { + + {{ 'security.password-requirement.password-should-difference' | translate }} + + } + @if (changePassword.get('newPassword').hasError('hasWhitespaces')) { + + {{ 'security.password-requirement.password-should-not-contain-spaces' | translate }} + + }
@@ -86,9 +96,11 @@ login.new-password-again - - {{ 'security.password-requirement.new-passwords-not-match' | translate }} - + @if (changePassword.get('newPassword2').hasError('passwordsNotMatch')) { + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + }
@@ -97,47 +109,61 @@
-
-

security.password-requirement.password-requirements

-

security.password-requirement.at-least

-

- - {{ changePassword.get('newPassword').hasError('notUpperCase') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.uppercase-letter' | translate : {count: passwordPolicy.minimumUppercaseLetters} }} -

-

- - {{ changePassword.get('newPassword').hasError('notLowerCase') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.lowercase-letter' | translate : {count: passwordPolicy.minimumLowercaseLetters} }} -

-

- - {{ changePassword.get('newPassword').hasError('notNumeric') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.digit' | translate : {count: passwordPolicy.minimumDigits} }} -

-

- - {{ changePassword.get('newPassword').hasError('notSpecial') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.special-character' | translate : {count: passwordPolicy.minimumSpecialCharacters} }} -

-

- - {{ changePassword.get('newPassword').hasError('minLength') ? 'mdi:circle-small' : 'mdi:check' }} - - {{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }} -

-
-

security.password-requirement.at-most

-

- - {{ 'security.password-requirement.character' | translate : {count: passwordPolicy.maximumLength} }} -

+ @if (passwordPolicy) { +
+

security.password-requirement.password-requirements

+

security.password-requirement.at-least

+ @if (passwordPolicy.minimumUppercaseLetters > 0) { +

+ + {{ changePassword.get('newPassword').hasError('notUpperCase') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.uppercase-letter' | translate : {count: passwordPolicy.minimumUppercaseLetters} }} +

+ } + @if (passwordPolicy.minimumLowercaseLetters > 0) { +

+ + {{ changePassword.get('newPassword').hasError('notLowerCase') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.lowercase-letter' | translate : {count: passwordPolicy.minimumLowercaseLetters} }} +

+ } + @if (passwordPolicy.minimumDigits > 0) { +

+ + {{ changePassword.get('newPassword').hasError('notNumeric') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.digit' | translate : {count: passwordPolicy.minimumDigits} }} +

+ } + @if (passwordPolicy.minimumSpecialCharacters > 0) { +

+ + {{ changePassword.get('newPassword').hasError('notSpecial') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.special-character' | translate : {count: passwordPolicy.minimumSpecialCharacters} }} +

+ } + @if (passwordPolicy.minimumLength > 0) { +

+ + {{ changePassword.get('newPassword').hasError('minLength') ? 'mdi:circle-small' : 'mdi:check' }} + + {{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }} +

+ } + @if (passwordPolicy.maximumLength > 0) { +
+

security.password-requirement.at-most

+

+ + {{ 'security.password-requirement.character' | translate : {count: passwordPolicy.maximumLength} }} +

+
+ }
-
+ }
- - - admin.2fa.2fa - - -
security.2fa.2fa-description
-
-

security.2fa.authenticate-with

-
- -
-

{{ providersData.get(provider).name | translate }}

-
-
- {{ providersData.get(provider).description | translate }} + @if (allowTwoFactorProviders.length) { + + + admin.2fa.2fa + + +
security.2fa.2fa-description
+
+

security.2fa.authenticate-with

+ + @for (provider of allowTwoFactorProviders; track trackByProvider($index, provider)) { +
+

{{ providersData.get(provider).name | translate }}

+
+ @if (!twoFactorAuth.get(provider).value) { +
+ {{ providersData.get(provider).description | translate }} +
+ } @else { +
+ {{ providersData.get(provider).activatedHint | translate: providerDataInfo(provider) }} +
+ } + +
- -
- {{ providersData.get(provider).activatedHint | translate: providerDataInfo(provider) }} -
-
- - + @if (twoFactorAuth.get(provider).value && provider !== twoFactorAuthProviderType.BACKUP_CODE && !activeSingleProvider) { + + security.2fa.main-2fa-method + + } + @if (twoFactorAuth.get(provider).value && provider === twoFactorAuthProviderType.BACKUP_CODE) { + + }
- - security.2fa.main-2fa-method - - -
- - - - + @if (!$last) { + + } + } + + + }
diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html index b24e5e247a..d8e4ab8bda 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html @@ -31,8 +31,10 @@ [entityName]="entity.name"> - - - + @if (!isEdit) { + + + + } } diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html index 83d4e47d85..0e9dea874a 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html @@ -51,12 +51,16 @@ tenant.title - - {{ 'tenant.title-required' | translate }} - - - {{ 'tenant.title-max-length' | translate }} - + @if (entityForm.get('title').hasError('required')) { + + {{ 'tenant.title-required' | translate }} + + } + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'tenant.title-max-length' | translate }} + + } tenant.description -
-
- - - {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} - + @if (entity?.id) { +
+
+ + + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} + +
-
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html index 1ac1ff6cf2..a5a6042e2e 100644 --- a/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html index 9dee500758..d2e86b8779 100644 --- a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html @@ -26,17 +26,23 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
user.activation-method - - {{ activationMethodTranslations.get(activationMethodEnum[activationMethod]) | translate }} - + @for (activationMethod of activationMethods; track activationMethod) { + + {{ activationMethodTranslations.get(activationMethodEnum[activationMethod]) | translate }} + + }
diff --git a/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html index 97ece30b0c..1046a3799e 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html @@ -15,32 +15,42 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - +@if (entity) { + + + + +} +@if (entity) { + + + + +} +@if (entity) { + + + +} +@if (entity && authUser.authority === authorities.TENANT_ADMIN) { + + + +} +@if (entity) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/user/user.component.html b/ui-ngx/src/app/modules/home/pages/user/user.component.html index e2d9fec8c0..789d2244b6 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.html @@ -46,13 +46,14 @@ [class.!hidden]="isEdit || isUserActivated()"> {{'user.resend-activation' | translate }} - + @if (loginAsUserEnabled$ | async) { + + } - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
{{ dialogTitle }} widget.title - - {{ 'widget.title-required' | translate }} - + @if (saveWidgetTypeAsFormGroup.get('title').hasError('required')) { + + {{ 'widget.title-required' | translate }} + + } close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
- + @for (type of allWidgetTypes; track type) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index c9c3f634ae..84744f6217 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -27,9 +27,11 @@ - - {{ widgetTypesDataMap.get(widgetTypes[type]).name | translate }} - + @for (type of allWidgetTypes; track type) { + + {{ widgetTypesDataMap.get(widgetTypes[type]).name | translate }} + + } @@ -48,15 +50,17 @@ undo action.undo - + @if (!isReadOnly) { + + } - + @if (!isReadOnly) { + + } -
+ @for (resource of widget.resources; track resource; let i = $index) { +
+ + + + {{ 'widget.resource-is-extension' | translate }} + + +
+ }
@@ -315,10 +327,12 @@
-
- -
+ @if (!iframeWidgetEditModeInited) { +
+ +
+ }
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html index e365b185e8..f6bb65468b 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html @@ -17,7 +17,9 @@ --> - {{ label }} + @if (label) { + {{ label }} + } - + @if (selectWidgetTypeFormGroup.get('widgetType').value && !disabled) { + + } - -
- {{ widgetType.name }} -
-
-
{{ 'widget.deprecated' | translate }}
+ @for (widgetType of filteredWidgetTypes | async; track widgetType) { + +
+ {{ widgetType.name }} +
+
+ @if (widgetType.deprecated) { +
{{ 'widget.deprecated' | translate }}
+ } +
-
- - -
-
- {{ 'widget.no-widgets-text' | translate }} + + } + @if (!(filteredWidgetTypes | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ 'widget.no-widgets-text' | translate }} +
+ } @else { + + {{ translate.get('widget.no-widgets-matching', {entity: searchText}) | async }} + + }
- - - {{ translate.get('widget.no-widgets-matching', {entity: searchText}) | async }} - - -
- + + } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html index 77957a8008..ec789ed6d2 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html @@ -15,9 +15,11 @@ limitations under the License. --> - - - +@if (isTenantWidgetType() && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html index c5c8ed259e..dd4b56bef8 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
- - - +@if (isTenantWidgetsBundle() && authUser.authority === authorities.TENANT_ADMIN) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html index 39308311ab..e56c3370ae 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html @@ -27,13 +27,14 @@ {{ widgetsBundle.title }}: {{ 'widget.widgets' | translate }} - + @if (!isReadOnly) { + + }
- - -
+ @if (isLoading$ | async) { + + + } + @if ((isLoading$ | async) === false) { +
+ }
-
-
- {{ widget.name }} -
-
{{ widget.name }}
-
{{ 'widget.deprecated' | translate }}
+ @for (widget of widgets; track trackByWidget($index, widget); let isLast = $last) { +
+
+ {{ widget.name }} +
+
{{ widget.name }}
+ @if (widget.deprecated) { +
{{ 'widget.deprecated' | translate }}
+ } +
+ @if (!editMode) { + + } + @if (!editMode) { + + } + @if (editMode && !addMode) { + + } + @if (editMode) { + + }
- - - - -
+ }
-
- add -
-
-
+ @if (editMode && !addMode && widgets?.length) { +
add -
widget.add-existing-widget
-
-
-
-
widget.no-widgets
-
-
- - -
- + } + @if (editMode && !addMode && !widgets?.length) { +
+
+ add +
widget.add-existing-widget
+
-
+ } + @if (isReadOnly && !widgets?.length) { +
+
+
widget.no-widgets
+
+ } + @if (addMode) { +
+ + +
+ +
+
+ } - - - + @if (editMode && (widgets?.length || isDirty)) { + + } + @if (editMode && (widgets?.length || isDirty)) { + + } + @if (!isReadOnly && !editMode) { + + } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html index 6546118554..89f2547118 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html @@ -15,44 +15,50 @@ limitations under the License. --> -
- - - - -
+@if (!standalone) { +
+ + + + +
+}
widgets-bundle.title - - {{ 'widgets-bundle.title-required' | translate }} - - - {{ 'widgets-bundle.title-max-length' | translate }} - + @if (entityForm.get('title').hasError('required')) { + + {{ 'widgets-bundle.title-required' | translate }} + + } + @if (entityForm.get('title').hasError('maxlength')) { + + {{ 'widgets-bundle.title-max-length' | translate }} + + } login.create-password
- - - + @if (isLoading || (isLoading$ | async)) { + + + } + @if (!isLoading && !(isLoading$ | async)) { + + } @@ -39,18 +43,22 @@ formControlName="newPassword"/> lock - - {{ 'security.password-requirement.password-not-meet-requirements' | translate }} - + @if (createPassword.get('newPassword').errors) { + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + } login.password-again lock - - {{ 'security.password-requirement.new-passwords-not-match' | translate }} - + @if (createPassword.get('newPassword2').hasError('passwordsNotMatch')) { + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + }
+ [passwordControl]="createPassword.get('newPassword')" + [passwordPolicy]="passwordPolicy" + [trigger]="passwordTrigger"> 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 495d6fa0cb..12e027ee73 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 @@ -76,7 +76,7 @@ tooltipPosition="above" icon="content_copy" [style]="{'font-size': '24px', color: 'rgba(255,255,255,.8)'}" - > + >
@@ -167,12 +167,16 @@ type="email" required autocomplete="email" placeholder="{{ 'login.email-label' | translate }}" /> - - {{ 'login.email-required' | translate }} - - - {{ 'login.invalid-email-format' | translate }} - + @if (emailConfigForm.get('email').hasError('required')) { + + {{ 'login.email-required' | translate }} + + } + @if (emailConfigForm.get('email').hasError('email')) { + + {{ 'login.invalid-email-format' | translate }} + + }
@@ -267,9 +271,9 @@ autocomplete="one-time-code" placeholder="{{ 'login.verification-code' | translate }}"> @if (configForm.get('verificationCode').getError('required') || - configForm.get('verificationCode').getError('minlength') || - configForm.get('verificationCode').getError('maxlength') || - configForm.get('verificationCode').getError('pattern')) { + configForm.get('verificationCode').getError('minlength') || + configForm.get('verificationCode').getError('maxlength') || + configForm.get('verificationCode').getError('pattern')) { {{ 'login.verification-code-invalid' | translate }} } @if (configForm.get('verificationCode').getError('incorrectCode')) { 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 39fd731fea..14f968d11e 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 @@ -22,9 +22,13 @@
- - - + @if (isLoading || (isLoading$ | async)) { + + + } + @if (!isLoading && !(isLoading$ | async)) { + + }
@if(oauth2Clients?.length) { @@ -60,9 +64,11 @@ login.username email - - {{ 'login.invalid-email-format' | translate }} - + @if (loginFormGroup.get('username').invalid) { + + {{ 'login.invalid-email-format' | translate }} + + } common.password @@ -71,7 +77,7 @@ lock + class="tb-action-button" type="submit">{{ 'login.sign-in' | translate }}
+ [passwordControl]="resetPassword.get('newPassword')" + [passwordPolicy]="passwordPolicy" + [trigger]="passwordTrigger"> 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 f1a58933fc..eb894f322e 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 @@ -27,70 +27,85 @@ -
-

login.select-way-to-verify

- - - -
- -
-
-

{{ providerDescription }}

-
- - - - {{ 'security.2fa.dialog.verification-code-invalid' | translate }} - - - {{ 'security.2fa.dialog.verification-code-incorrect' | translate }} - - - {{ 'security.2fa.dialog.verification-code-many-request' | translate }} - - -
- - - -
-
-
- {{ 'login.resend-code-wait' | translate : {time: countDownTime} }} -
- + } +
+ } + @if (selectedProvider) { + +
+
+

{{ providerDescription }}

+
+ + + @if (verificationForm.get('verificationCode').getError('required') || + verificationForm.get('verificationCode').getError('minlength') || + verificationForm.get('verificationCode').getError('maxlength') || + verificationForm.get('verificationCode').getError('pattern')) { + + {{ 'security.2fa.dialog.verification-code-invalid' | translate }} + + } + @if (verificationForm.get('verificationCode').getError('incorrectCode')) { + + {{ 'security.2fa.dialog.verification-code-incorrect' | translate }} + + } + @if (verificationForm.get('verificationCode').getError('tooManyRequest')) { + + {{ 'security.2fa.dialog.verification-code-many-request' | translate }} + + } +
- + +
+ @if (showResendAction) { +
+ @if (countDownTime) { +
+ {{ 'login.resend-code-wait' | translate : {time: countDownTime} }} +
+ } + +
+ } + @if (allowProviders.length > 1) { + + } +
-
-
- + + + }
diff --git a/ui-ngx/src/app/shared/components/button/copy-button.component.html b/ui-ngx/src/app/shared/components/button/copy-button.component.html index d664b4d097..c72cfdcda3 100644 --- a/ui-ngx/src/app/shared/components/button/copy-button.component.html +++ b/ui-ngx/src/app/shared/components/button/copy-button.component.html @@ -23,10 +23,11 @@ [matTooltip]="matTooltipText" [matTooltipPosition]="matTooltipPosition" (click)="copy($event)"> - - {{ icon || mdiIcon }} - - + @if (!copied) { + + {{ icon || mdiIcon }} + + } @else { done - + } diff --git a/ui-ngx/src/app/shared/components/button/toggle-password.component.html b/ui-ngx/src/app/shared/components/button/toggle-password.component.html index 07e91101ab..e284af02c4 100644 --- a/ui-ngx/src/app/shared/components/button/toggle-password.component.html +++ b/ui-ngx/src/app/shared/components/button/toggle-password.component.html @@ -15,6 +15,8 @@ limitations under the License. --> - +@if (!hideToggle) { + +} diff --git a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html index fb0c8bfab9..3684b29e5b 100644 --- a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html +++ b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html @@ -27,16 +27,28 @@ [class.tb-disabled-state]="disabled" [class.tb-disabled]="disabled"> -
- {{ appearance.leftAppearance.icon }} - {{ leftLabel$ | async }} -
+ @if (appearance.leftAppearance.showIcon || appearance.leftAppearance.showLabel) { +
+ @if (appearance.leftAppearance.showIcon) { + {{ appearance.leftAppearance.icon }} + } + @if (appearance.leftAppearance.showLabel) { + {{ leftLabel$ | async }} + } +
+ }
-
- {{ appearance.rightAppearance.icon }} - {{ rightLabel$ | async }} -
+ @if (appearance.rightAppearance.showIcon || appearance.rightAppearance.showLabel) { +
+ @if (appearance.rightAppearance.showIcon) { + {{ appearance.rightAppearance.icon }} + } + @if (appearance.rightAppearance.showLabel) { + {{ rightLabel$ | async }} + } +
+ }
diff --git a/ui-ngx/src/app/shared/components/button/widget-button.component.html b/ui-ngx/src/app/shared/components/button/widget-button.component.html index 2ff5d5e07f..16cf5fd557 100644 --- a/ui-ngx/src/app/shared/components/button/widget-button.component.html +++ b/ui-ngx/src/app/shared/components/button/widget-button.component.html @@ -33,8 +33,14 @@ [class.tb-disabled-state]="disabled" [style.border-radius]="computedBorderRadius" [style.pointer-events]="disableEvents ? 'none' : ''"> -
- {{ appearance.icon }} - {{ label$ | async }} -
+ @if (appearance.showIcon || appearance.showLabel) { +
+ @if (appearance.showIcon) { + {{ appearance.icon }} + } + @if (appearance.showLabel) { + {{ label$ | async }} + } +
+ } diff --git a/ui-ngx/src/app/shared/components/cheatsheet.component.ts b/ui-ngx/src/app/shared/components/cheatsheet.component.ts index 00fe6e5790..41fa0ce39f 100644 --- a/ui-ngx/src/app/shared/components/cheatsheet.component.ts +++ b/ui-ngx/src/app/shared/components/cheatsheet.component.ts @@ -115,17 +115,23 @@ import Mousetrap from 'mousetrap'; } } `], template: ``, +

{{ title }}

+ @if (helpVisible) { + + @for (hotkey of hotkeysList; track hotkey) { + + + + + } +
+ @for (key of hotkey.formatted; track key) { + {{ key }} + } + {{ hotkey.description }}
+ } +
×
+
`, standalone: false }) export class TbCheatSheetComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html index 92c360e8c7..d2f21661bc 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -15,26 +15,37 @@ limitations under the License. --> - - - {{icon}} - {{label}} - -
-
-
- - - - {{ requiredText }} - -
- +@if (!asBoxInput) { + + @if (icon || label) { + + @if (icon) { + {{icon}} + } + @if (label) { + {{label}} + } + + } +
+
+
+ + @if (colorClearButton && !disabled) { + + } + @if (colorFormGroup.get('color').hasError('required')) { + + {{ requiredText }} + + } +
+} @else {
- +} diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html index 0c58364c17..088c48bd15 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html @@ -18,22 +18,26 @@
- + @if (colorClearButton) { + + }
- + @if (colorCancelButton) { + + }
diff --git a/ui-ngx/src/app/shared/components/country-autocomplete.component.html b/ui-ngx/src/app/shared/components/country-autocomplete.component.html index befb160df6..1e3ffecac5 100644 --- a/ui-ngx/src/app/shared/components/country-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/country-autocomplete.component.html @@ -29,38 +29,51 @@ (focusin)="onFocus()" (blur)="checkInputAndAutoSelect()" [matAutocomplete]="countryAutocomplete"> - + @if (countryFormGroup.get('country').value && !disabled) { + + } - - {{country.flag}} - - - -
-
- contact.no-country-found -
- - + @for (country of filteredCountries | async; track country) { + + {{country.flag}} + + + } + @if (!(filteredCountries | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ contact.no-country-found +
+ } @else { + {{ 'contact.no-country-matching' | translate: - {country: (searchText | truncate: true: 6: '...')} }} - - -
-
+ {country: (searchText | truncate: true: 6: '...')} }} +
+ } +
+
+ }
- {{ autocompleteHint }} - - {{ requiredText }} - - - {{ objectRequiredText }} - + @if (autocompleteHint) { + {{ autocompleteHint }} + } + @if (countryFormGroup.get('country').hasError('required')) { + + {{ requiredText }} + + } + @if (countryFormGroup.get('country').hasError('objectRequired')) { + + {{ objectRequiredText }} + + } diff --git a/ui-ngx/src/app/shared/components/css.component.html b/ui-ngx/src/app/shared/components/css.component.html index 9729b97e06..b0050c974f 100644 --- a/ui-ngx/src/app/shared/components/css.component.html +++ b/ui-ngx/src/app/shared/components/css.component.html @@ -21,9 +21,11 @@
- + @if (!disabled) { + + }
- {{ label }} + class="mat-block" + [floatLabel]="floatLabel" + [appearance]="inlineField ? 'outline' : appearance" + [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> + @if (!inlineField && label) { + {{ label }} + } - - {{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) | customTranslate }} - - - - warning - + @if (useDashboardLink && selectDashboardFormGroup.get('dashboard').value && disabled) { + + {{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) | customTranslate }} + + } + @if (selectDashboardFormGroup.get('dashboard').value && !disabled) { + + } + @if (inlineField && requiredText && selectDashboardFormGroup.get('dashboard').hasError('required') + && selectDashboardFormGroup.get('dashboard').touched) { + + warning + + } - - - - -
-
- {{ 'dashboard.no-dashboards-text' | translate }} + @for (dashboard of filteredDashboards | async; track dashboard) { + + + + } + @if (!(filteredDashboards | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ 'dashboard.no-dashboards-text' | translate }} +
+ } @else { + + {{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }} + + }
- - - {{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }} - - -
- + + } - - - - - - + @if (!inlineField) { + + + + } + @if (!inlineField) { + + + + } diff --git a/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html index b26352b817..2342210dba 100644 --- a/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html @@ -20,9 +20,11 @@ {{'dashboard.select-dashboard' | translate}} - - {{dashboard.title}} - + @for (dashboard of dashboards$ | async; track dashboard) { + + {{dashboard.title}} + + }
diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.html b/ui-ngx/src/app/shared/components/dashboard-select.component.html index f3b278cd59..b617afe9d1 100644 --- a/ui-ngx/src/app/shared/components/dashboard-select.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-select.component.html @@ -20,9 +20,11 @@ [disabled]="disabled" [(ngModel)]="dashboardId" (ngModelChange)="dashboardIdChanged()"> - - {{dashboard.title}} - + @for (dashboard of dashboards$ | async; track dashboard) { + + {{dashboard.title}} + + }
+ @if (selectDashboardStateFormGroup.get('dashboardStateId').value) { + + } - - - + @for (state of filteredStatesDashboard$ | async; track state) { + + + + } - - {{ 'widget-action.target-dashboard-state-required' | translate }} - + @if (selectDashboardStateFormGroup.get('dashboardStateId').hasError('required')) { + + {{ 'widget-action.target-dashboard-state-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html index 0778e2ecd6..d2e07083f6 100644 --- a/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html @@ -16,12 +16,11 @@ -->

{{data.title}}

- +@if (data.textMode) {
-
- +} @else {
-
+}
diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts index fe173be672..b50670e693 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts @@ -18,7 +18,6 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { SharedModule } from '@shared/shared.module'; import { ImportExportService } from '@shared/import-export/import-export.service'; -import { CommonModule } from '@angular/common'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityInfoData, VersionedEntity } from '@shared/models/entity.models'; import { EntityId } from '@shared/models/id/entity-id'; @@ -34,9 +33,8 @@ interface EntityConflictDialogData { templateUrl: 'entity-conflict-dialog.component.html', styleUrls: ['./entity-conflict-dialog.component.scss'], imports: [ - CommonModule, - SharedModule, - ] + SharedModule +] }) export class EntityConflictDialogComponent { diff --git a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html index 379a7266db..73c19ea831 100644 --- a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html @@ -20,10 +20,12 @@
{{ message }}
dialog.error-message-title
{{ errorMessage }}
- - {{ 'dialog.error-details-title' | translate }} - - + @if (errorDetails) { + + {{ 'dialog.error-details-title' | translate }} + + + }
diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 8a576133f9..6624bd5e5d 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -22,7 +22,9 @@ [appearance]="inlineField ? 'outline' : appearance" [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing" [class]="additionalClasses"> - {{ label | translate }} + @if (!inlineField) { + {{ label | translate }} + } - - {{ displayEntityFn(selectEntityFormGroup.get('entity').value) }} - - - - - warning - + @if (showEntityLink) { + + {{ displayEntityFn(selectEntityFormGroup.get('entity').value) }} + + } + @if (selectEntityFormGroup.get('entity').value && !disabled) { + + } + @if (allowCreateNew && !selectEntityFormGroup.get('entity').value && !disabled) { + + } + @if (inlineField && selectEntityFormGroup.get('entity').hasError('required') + && selectEntityFormGroup.get('entity').touched) { + + warning + + } - - - - -
-
- {{ notFoundEntities | translate }} -
- - - {{ noEntitiesMatchingText | translate: {entity: searchText} }} - - @if (allowCreateNew) { + @for (entity of filteredEntities | async; track entity) { + + + + } + @if (!(filteredEntities | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ notFoundEntities | translate }} +
+ } @else { - entity.create-new-key + {{ noEntitiesMatchingText | translate: {entity: searchText} }} + @if (allowCreateNew) { + + entity.create-new-key + + } } - -
-
+
+
+ }
- - {{ requiredErrorText | translate }} - + @if (!inlineField && selectEntityFormGroup.get('entity').hasError('required')) { + + {{ requiredErrorText | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.html index f143fd227c..1b4e58c21e 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.html @@ -23,34 +23,43 @@ (keydown)="gatewayNameEnter($event)" (keypress)="gatewayNameEnter($event)" [matAutocomplete]="gatewayAutocomplete" - > - + > + @if (selectDeviceGatewayFormGroup.get('gateway').value && !isStateForm) { + + } - - - - -
-
- gateway.no-gateway-found -
- - - gateway.no-gateway-matching + @for (gateway of filteredGateways | async; track gateway) { + + + + } + @if (!(filteredGateways | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ gateway.no-gateway-found +
+ } @else { + + gateway.no-gateway-matching + + } + + gateway.create-new-gateway - - - gateway.create-new-gateway - -
-
+
+
+ }
- - {{ 'gateway.gateway-name-required' | translate }} - + @if (selectDeviceGatewayFormGroup.get('gateway').hasError('required')) { + + {{ 'gateway.gateway-name-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.html index 7f7f3d0b40..53c26e0bff 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-keys-list.component.html @@ -17,13 +17,16 @@ --> - - {{key}} - close - + @for (key of modelValue; track key) { + + {{key}} + @if (!disabled) { + close + } + + } - - - + @for (key of filteredKeys | async; track key) { + + + + } diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html index 0469adca74..6a94f1fc04 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html @@ -16,27 +16,29 @@ -->
- - - - + @if (displayEntityTypeSelect) { + + + } + @if (modelValue.entityType) { + + + }
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html index 589019b9fa..8781908d82 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -21,16 +21,21 @@ [class.tb-chips]="inlineField" [class.flex]="inlineField" [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> - {{ labelText }} + @if (!inlineField && labelText) { + {{ labelText }} + } - - {{ displayEntityFn(entity) }} - close - + @for (entity of entities; track entity) { + + {{ displayEntityFn(entity) }} + @if (!disabled) { + close + } + + } - + @if (allowCreateNew && !disabled) { + + } - - - - -
-
- {{ 'entity.no-entities-text' | translate }} -
- - - {{ 'entity.no-entities-matching' | translate: {entity: searchText} }} - - @if (allowCreateNew) { + @for (entity of filteredEntities | async; track entity) { + + + + } + @if (!(filteredEntities | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ 'entity.no-entities-text' | translate }} +
+ } @else { - entity.create-new-key + {{ 'entity.no-entities-matching' | translate: {entity: searchText} }} + @if (allowCreateNew) { + + entity.create-new-key + + } } - -
-
+
+
+ }
- - {{ hint }} - - - {{ requiredText }} - + @if (!inlineField && hint) { + + {{ hint }} + + } + @if (!inlineField && entityListFormGroup.get('entities').hasError('required')) { + + {{ requiredText }} + + }
diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index 59d5972c65..e76db03f77 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -16,28 +16,30 @@ -->
- - - - + @if (displayEntityTypeSelect) { + + + } + @if (modelValue.entityType && !entityTypeNullUUID.has(modelValue.entityType)) { + + + }
diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html index 942ef0632b..117742515b 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html @@ -23,24 +23,32 @@ (focusin)="onFocus()" [required]="required" [matAutocomplete]="subTypeAutocomplete"> - + @if (subTypeFormGroup.get('subType').value && !disabled) { + + } - - - + @for (subType of filteredSubTypes | async; track subType) { + + + + } - - {{ entitySubtypeRequiredText | translate }} - - - {{ entitySubtypeMaxLength | translate }} - + @if (subTypeFormGroup.get('subType').hasError('required')) { + + {{ entitySubtypeRequiredText | translate }} + + } + @if (subTypeFormGroup.get('subType').hasError('maxlength')) { + + {{ entitySubtypeMaxLength | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html index b6320bacf7..261ecea281 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html @@ -18,16 +18,21 @@ - {{ label }} + @if (label) { + {{ label }} + } - - {{customTranslate(entitySubtype)}} - close - + @for (entitySubtype of entitySubtypeList; track entitySubtype) { + + {{customTranslate(entitySubtype)}} + @if (!disabled) { + close + } + + } - - - + @for (entitySubtype of filteredEntitySubtypeList | async; track entitySubtype) { + + + + }
- - {{ subtypeListEmptyText | translate }} - + @if (entitySubtypeListFormGroup.get('entitySubtypeList').hasError('required')) { + + {{ subtypeListEmptyText | translate }} + + }
diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html index f9fa081c4d..1f62aa4f7b 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html @@ -16,13 +16,19 @@ --> - {{ entitySubtypeTitle | translate }} + @if (showLabel) { + {{ entitySubtypeTitle | translate }} + } - - {{ displaySubTypeFn(subType) }} - + @for (subType of subTypesOptions | async; track subType) { + + {{ displaySubTypeFn(subType) }} + + } - - {{ entitySubtypeRequiredText | translate }} - + @if (subTypeFormGroup.get('subType').hasError('required')) { + + {{ entitySubtypeRequiredText | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html index 15b4c6ae5e..f929e34199 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-type-list.component.html @@ -20,15 +20,20 @@ class="mat-block" [class]="additionalClasses" [floatLabel]="floatLabel" subscriptSizing="{{ subscriptSizing }}"> - {{ label }} + @if (label) { + {{ label }} + } - - {{entityType.name}} - close - + @for (entityType of entityTypeList; track entityType) { + + {{entityType.name}} + @if (!disabled) { + close + } + + } - - - - - + @for (entityType of filteredEntityTypeList | async; track entityType) { + + + + } + @if (!(filteredEntityTypeList | async)?.length) { + + {{ translate.get('entity.no-entity-types-matching', {entityType: searchText}) | async }} - - + + + }
- - {{ 'entity.entity-type-list-required' | translate }} - + @if (entityTypeListFormGroup.get('entityTypeList').hasError('required')) { + + {{ 'entity.entity-type-list-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html index 65e3615887..e13e11bde0 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html @@ -18,13 +18,19 @@ - {{ label }} + @if (showLabel && !inlineField) { + {{ label }} + } - - {{ displayEntityTypeFn(type) }} - + @for (type of entityTypes; track type) { + + {{ displayEntityTypeFn(type) }} + + } - - {{ 'entity.type-required' | translate }} - + @if (!inlineField && entityTypeFormGroup.get('entityType').hasError('required')) { + + {{ 'entity.type-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts index ffc62ee7c2..99d3a83c64 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts @@ -173,9 +173,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, } } - displayEntityTypeFn(entityType?: EntityType | AliasEntityType | null): string | undefined { - if (this.additionEntityTypes[entityType]) { - return this.additionEntityTypes[entityType]; + displayEntityTypeFn(entityType?: EntityType | AliasEntityType | string | null): string | undefined { + if (this.additionEntityTypes[entityType as EntityType]) { + return this.additionEntityTypes[entityType as EntityType]; } else if (entityType) { return this.translate.instant(entityTypeTranslations.get(entityType as EntityType).type); } else { diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index 86e1d4c7e5..44b5442fac 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -16,11 +16,13 @@ -->
- + @if (label) { + + }
@@ -43,7 +45,7 @@ {{ dropLabel }}
- -
{{ noFileText }}
-
{{ fileName }}
-
dashboard.maximum-upload-file-size
+ @if (!fileName && required && requiredAsError) { + + } + @if (!fileName && !requiredAsError) { +
{{ noFileText }}
+ } + @if (fileName) { +
{{ fileName }}
+ } + @if (maxSizeByte && !disabled) { +
dashboard.maximum-upload-file-size
+ }
diff --git a/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html index 1ea0f1d6da..d2d408e9ff 100644 --- a/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html +++ b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html @@ -26,14 +26,16 @@ {{ footerFabButtons.fabTogglerIcon }}
- + @for (btn of buttons; track btn) { + + }
diff --git a/ui-ngx/src/app/shared/components/grid/scroll-grid.component.html b/ui-ngx/src/app/shared/components/grid/scroll-grid.component.html index d83594e0b7..ef57d199dc 100644 --- a/ui-ngx/src/app/shared/components/grid/scroll-grid.component.html +++ b/ui-ngx/src/app/shared/components/grid/scroll-grid.component.html @@ -18,28 +18,31 @@ -
-
- - - - - - + @if (itemsRow) { +
+ @for (item of itemsRow; track trackByItem(col, item); let col = $index) { +
+ @if (item === 'loadingCell') { + + } + @if (isObject(item)) { + + } +
+ }
-
+ } - +@if (dataSource.isEmpty) { - +} - + @if (dataSource.initialDataLoading) { - - - - + } @else { + + }
diff --git a/ui-ngx/src/app/shared/components/help-popup.component.html b/ui-ngx/src/app/shared/components/help-popup.component.html index bec404b063..65edb22cc3 100644 --- a/ui-ngx/src/app/shared/components/help-popup.component.html +++ b/ui-ngx/src/app/shared/components/help-popup.component.html @@ -15,44 +15,54 @@ limitations under the License. --> -
-
- -
-
-
-
- +
+
+} +@if (textMode) { +
+
+ -
-
+ + @if (triggerSafeHtml) { + + } + @if ((!popoverVisible || popoverReady)) { + open_in_new + } + @if (popoverVisible && !popoverReady) { + + } + + +
+
+} diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html index ae84c5633f..f2f0983975 100644 --- a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html @@ -16,7 +16,8 @@ --> -{{ hintIcon }} +@if (tooltipText) { + {{ hintIcon }} +} diff --git a/ui-ngx/src/app/shared/components/html.component.html b/ui-ngx/src/app/shared/components/html.component.html index f17272bf71..6520340374 100644 --- a/ui-ngx/src/app/shared/components/html.component.html +++ b/ui-ngx/src/app/shared/components/html.component.html @@ -21,9 +21,11 @@
- + @if (!disabled) { + + }
- + @if (label) { + + }
-
-
-
{{ 'dashboard.empty-image' | translate }}
- -
- -
-
-
- cloud_upload -
- image-input.drag-and-drop -
- image-input.or - - -
+ @if (showPreview && (safeImageUrl || disabled)) { +
+
+ @if (!safeImageUrl && disabled) { +
{{ 'dashboard.empty-image' | translate }}
+ } @else { + + }
-
-
- -
+ close + + } +
+ } + @if (!disabled) { +
+
+ cloud_upload +
+ image-input.drag-and-drop +
+ image-input.or + + +
+
+
+
+ } + @if (!showPreview && safeImageUrl && showClearButton && !disabled) { + + } +
-
-
{{ fileName }}
-
dashboard.maximum-upload-file-size
-
+ @if ((showFileName && fileName) || (maxSizeByte && !disabled)) { +
+ @if (showFileName && fileName) { +
{{ fileName }}
+ } + @if (maxSizeByte && !disabled) { +
dashboard.maximum-upload-file-size
+ } +
+ }
diff --git a/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html index cf6a054af4..c9446437b7 100644 --- a/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html @@ -24,36 +24,46 @@ close - - -
+@if (isLoading$ | async) { + + +} +@if (!(isLoading$ | async)) { +
+}
-
- - - -
- -
{{ 'image.embed-to-html' | translate }}
-
-
-
image.embed-to-html
-
-
- -
- -
-
-
+ @if (!readonly || image.public) { +
+ + + + @if (!readonly) { +
+ +
{{ 'image.embed-to-html' | translate }}
+
+
+ } + @if (readonly) { +
image.embed-to-html
+ } +
+
+ +
+ +
+
+
+ }
image.embed-to-angular-template
+ [data]="embedToAngularTemplateCode()">
diff --git a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html index 2ebe68d897..be6344ffae 100644 --- a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html +++ b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html @@ -16,79 +16,95 @@ -->
- + @if (label) { + + }
- + @if (linkType !== ImageLinkType.none && imageUrl) { + + } @else { +
{{ (disabled ? 'image.no-image' : 'image.no-image-selected') | translate }}
+ }
-
-
-
-
- -
- {{ imageResource.title }} + @if (linkType !== ImageLinkType.none && (linkType !== ImageLinkType.base64 || !disabled)) { +
+ @if (linkType === ImageLinkType.base64) { +
-
-
{{ imageResource.descriptor.width }}x{{ imageResource.descriptor.height }}
- -
{{ imageResource.descriptor.size | fileSize }}
+ } + @if (linkType === ImageLinkType.resource) { +
+ @if (!loadingImageResource) { +
+ {{ imageResource.title }} +
+
+
{{ imageResource.descriptor.width }}x{{ imageResource.descriptor.height }}
+ +
{{ imageResource.descriptor.size | fileSize }}
+
+ } @else { + + }
- -
-
- - +
+ } + @if (!disabled && linkType !== ImageLinkType.external) { + -
+ }
- -
-
- - -
+ } + @if (linkType === ImageLinkType.none && !disabled) { +
+ + +
+ }
- -
{{ (disabled ? 'image.no-image' : 'image.no-image-selected') | translate }}
-
- - - diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.html b/ui-ngx/src/app/shared/components/image/image-dialog.component.html index 0eefce39cf..d0ff71778c 100644 --- a/ui-ngx/src/app/shared/components/image/image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.html @@ -25,36 +25,46 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
image.name - - {{ 'image.name-required' | translate }} - - - + @if (imageFormGroup.get('title').hasError('required')) { + + {{ 'image.name-required' | translate }} + + } + @if (!readonly && imageFormGroup.dirty) { + + } + @if (!readonly && !imageFormGroup.invalid && imageFormGroup.dirty) { + + }
@@ -82,14 +92,16 @@ code
- + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.html b/ui-ngx/src/app/shared/components/image/image-gallery.component.html index b31dc16920..8e4bd5a294 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.html +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.html @@ -24,7 +24,9 @@
{{ (isScada ? 'scada.symbols' : 'image.gallery') | translate }}
-
+ @if (isScada) { +
+ }
@@ -44,10 +46,12 @@
- {{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }} + @if (!isSysAdmin) { + {{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }} + }
@@ -94,9 +98,11 @@
- {{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }} + @if (!isSysAdmin) { + {{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }} + }
@@ -132,200 +138,208 @@
-
-
- - - - - - - - - - - - - - -
- {{ image.title }} -
-
-
- - {{ 'image.name' | translate }} - - {{ image.title }} - - - - {{ 'image.created-time' | translate }} - - {{ image.createdTime | date:'yyyy-MM-dd HH:mm:ss' }} - - - - {{ 'image.resolution' | translate }} - - {{ image.descriptor.width }}x{{ image.descriptor.height }} - - - - {{ 'image.size' | translate }} - - {{ image.descriptor.size | fileSize }} - - - - {{ 'image.system' | translate }} - - {{isSystem(image) ? 'check_box' : 'check_box_outline_blank'}} - - - - - - -
- - - - - -
-
- - -
+ + + + + + + + + + + + + +
+ {{ image.title }} +
+
+
+ + {{ 'image.name' | translate }} + + {{ image.title }} + + + + {{ 'image.created-time' | translate }} + + {{ image.createdTime | date:'yyyy-MM-dd HH:mm:ss' }} + + + + {{ 'image.resolution' | translate }} + + {{ image.descriptor.width }}x{{ image.descriptor.height }} + + + + {{ 'image.size' | translate }} + + {{ image.descriptor.size | fileSize }} + + + + {{ 'image.system' | translate }} + + {{isSystem(image) ? 'check_box' : 'check_box_outline_blank'}} + + + + + + +
+ - - - + } + - - -
-
-
- - - - - - - - -
- - - - {{ 'common.loading' | translate }} +
+
+ + + + + @if (!isScada) { + + } + + + +
+ + + + + + + + + + + + @if ((dataSource.isEmpty() | async) && !dataSource.dataLoading) { + + } + {{ 'common.loading' | translate }} +
+ +
- - -
-
- - -
+ } + @if (mode === 'grid') { +
+ + +
+ }
- - {{ 'common.loading' | translate }} - + + {{ 'common.loading' | translate }} +
@@ -337,49 +351,57 @@
-
- - - - -
+ @if (!selectionMode) { +
+ + + @if (!isScada) { + + } + @if (deleteEnabled(item)) { + + } +
+ }
- - + @if (!selectionMode) { + + } + @if (selectionMode) { + + }
{{ item.title }} @@ -389,7 +411,9 @@
{{ item.title }}
-
sys
+ @if (isSystem(item)) { +
sys
+ }
{{ item.descriptor.width }}x{{ item.descriptor.height }}
diff --git a/ui-ngx/src/app/shared/components/image/image-references.component.html b/ui-ngx/src/app/shared/components/image/image-references.component.html index 32ce614dc2..421dac3caf 100644 --- a/ui-ngx/src/app/shared/components/image/image-references.component.html +++ b/ui-ngx/src/app/shared/components/image/image-references.component.html @@ -15,40 +15,45 @@ limitations under the License. --> - +@if (simpleList) { - - - +} @else { + @if (contentReady) {
    - -
  • -
    {{ 'image.system-entities' | translate }}
    - -
  • - + @for (entry of referencedEntitiesEntries; track entry) { + @if (isSystem(entry[0])) { +
  • +
    {{ 'image.system-entities' | translate }}
    + +
  • + } @else {
  • {{ 'tenant.tenant' | translate }} {{ entry[1].tenantName }} {{ 'image.entities' | translate }}
  • -
    -
    + } + }
-
-
+ } @else { + + } +} - - - - + @for (referencedEntity of entriesList; track referencedEntity) { + + + + + }
{{ referencedEntity.typeName }} - {{ referencedEntity.entity.name }} - {{ referencedEntity.entity.name }} -
{{ referencedEntity.typeName }} + @if (showDetailsLink) { + {{ referencedEntity.entity.name }} + } + @if (!showDetailsLink) { + {{ referencedEntity.entity.name }} + } +
- - - diff --git a/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html index 3108e6a67c..9844484987 100644 --- a/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html +++ b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html @@ -16,95 +16,114 @@ -->
- + @if (label) { + + }
-
- {{ 'image-input.images' | translate }} [{{ $index }}] -
-
- drag_indicator -
-
- -
-
- + @for (imageUrl of imageUrls; track imageUrl) { +
+ {{ 'image-input.images' | translate }} [{{ $index }}] +
+ @if (!disabled) { +
+ drag_indicator +
+ } +
+ +
+ @if (!disabled) { +
+ +
+ }
-
-
{{ 'image-input.no-images' | translate }}
+ } + @if (!imageUrls.length) { +
{{ 'image-input.no-images' | translate }}
+ }
-
-
- -
-
-
- + } + @if (linkType === ImageLinkType.none) { +
+ -
-
+ }
-
- - -
-
+ }
- -
{{ 'image.no-image-selected' | translate }}
-
diff --git a/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.html b/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.html index abaa800acf..99068df58e 100644 --- a/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.html +++ b/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.html @@ -16,50 +16,62 @@ -->
- + @if (label) { + + }
- + @if (linkType !== ScadaSymbolLinkType.none && scadaSymbolUrl) { + + } @else { +
{{ (disabled ? 'scada.no-symbol' : 'scada.no-symbol-selected') | translate }}
+ }
-
-
- -
- {{ imageResource.title | customTranslate }} + @if (linkType !== ScadaSymbolLinkType.none && !disabled) { +
+ @if (linkType === ScadaSymbolLinkType.resource) { +
+ @if (!loadingImageResource) { +
+ {{ imageResource.title | customTranslate }} +
+ } @else { + + }
- + } + @if (linkType === ScadaSymbolLinkType.content) { +
+
+ {{ scadaSymbolMetadata.title | customTranslate }} +
+
+ } + @if (linkType === ScadaSymbolLinkType.resource && !disabled) { + + }
-
-
- {{ scadaSymbolMetadata.title | customTranslate }} -
+ } + @if (linkType === ScadaSymbolLinkType.none && !disabled) { +
+
- -
-
- -
+ }
- -
{{ (disabled ? 'scada.no-symbol' : 'scada.no-symbol-selected') | translate }}
-
- - - diff --git a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html index 61da3149b9..bc1c5c2cfb 100644 --- a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html @@ -25,9 +25,13 @@ close - - -
+ @if (isLoading$ | async) { + + + } + @if (!(isLoading$ | async)) { +
+ }
- - image.name - - - {{ 'image.name-required' | translate }} - - + @if (uploadImage && uploadImageFormGroup.get('file').value) { + + image.name + + @if (uploadImageFormGroup.get('title').hasError('required')) { + + {{ 'image.name-required' | translate }} + + } + + }
diff --git a/ui-ngx/src/app/shared/components/js-func-module-row.component.html b/ui-ngx/src/app/shared/components/js-func-module-row.component.html index 2b60f9071c..5e6463ddd5 100644 --- a/ui-ngx/src/app/shared/components/js-func-module-row.component.html +++ b/ui-ngx/src/app/shared/components/js-func-module-row.component.html @@ -18,14 +18,15 @@
- - warning - + @if (moduleRowFormGroup.get('alias').invalid && moduleRowFormGroup.get('alias').touched) { + + warning + + }
+ help-icon-tooltip="{{ 'js-func.show-module-info' | translate }}">
+ help-icon-tooltip="{{ 'js-func.show-module-source-code' | translate }}">
-
-
- - + @if (modulesFormArray().controls.length) { +
+ @for (moduleControl of modulesFormArray().controls; track trackByModule($index, moduleControl)) { +
+ + +
+ }
-
- + } @else { + {{ 'js-func.no-modules' | translate }} + } + @if (modulesFormGroup.hasError('moduleAliasNotUnique')) { + + }
- - {{ 'js-func.no-modules' | translate }} - diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index e5d663152d..6cd69d6301 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -19,29 +19,37 @@ tb-fullscreen [fullscreen]="fullscreen">
- + @if (!hideLabel) { + + } -
-
- -
-
- + @if (!disabled && withModules) { +
+
+ +
+
+ } + @if (!disabled) { + + } -
+ @if (helpId) { +
+ }
-
- -
+ @if (!hideBrackets) { +
+ +
+ }
diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 67bdbf732c..665c78d17a 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -18,28 +18,34 @@
-
- - - - -
-
- -
-
-
+ } + @if (!readonly && !disabled) { + + } +
+
+ +
+
+
+ }
diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.html b/ui-ngx/src/app/shared/components/json-object-edit.component.html index 3c18ae38d6..13d2c5fa8a 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.html +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.html @@ -24,14 +24,18 @@ 'tb-readonly': readonly, 'tb-error': !objectValid}">{{ label }} - - + @if (!readonly && !disabled) { + + } + @if (!readonly && !disabled) { + + } -
+ @for (keyValControl of keyValsFormArray().controls; track keyValControl) { +
+ + + + + + + +
+ } {{noDataText ? noDataText : 'key-val.no-data'}} + class="no-data-found flex items-center justify-center" translate>{{noDataText ? noDataText : 'key-val.no-data'}}
+ mat-button (click)="toggleEditMode()">{{ 'markdown.edit' | translate }} -
+ mat-button (click)="toggleEditMode()">{{ 'markdown.preview' | translate }} + @if (helpId) { +
+ }
-
- -
+ @if (readonly || disabled) { +
+ +
+ }
diff --git a/ui-ngx/src/app/shared/components/markdown.component.html b/ui-ngx/src/app/shared/components/markdown.component.html index 62709f4f90..203900dd96 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.html +++ b/ui-ngx/src/app/shared/components/markdown.component.html @@ -17,10 +17,12 @@ --> -
- {{error}} -
+ {{error}} +
+}
diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.html b/ui-ngx/src/app/shared/components/material-icon-select.component.html index 59566607be..dc22921c7e 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.html +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -15,20 +15,23 @@ limitations under the License. --> -
- {{materialIconFormGroup.get('icon').value}} - - {{ label }} - - - -
- +@if (!asBoxInput) { +
+ {{materialIconFormGroup.get('icon').value}} + + {{ label }} + + @if (iconClearButton) { + + } + +
+} @else { -
+} diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index 97a661bfb1..bb9e3fca58 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -17,7 +17,9 @@ -->
- icon.icons + @if (showTitle) { + icon.icons + } @if (allowedCustomIcon) { {{ 'resource.system' | translate }} @@ -30,61 +32,73 @@ search - + @if (searchIconControl.value) { + + }
- - - - + @for (icon of iconRow; track icon) { + @if (icon.name === selectedIcon) { + + } + @if (icon.name !== selectedIcon) { + + } + }
- + @if (notFound) {
{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
-
-
- - - -
+ } + @if (iconClearButton || !showAllSubject.value) { +
+ @if (iconClearButton) { + + } + + @if (!showAllSubject.value) { + + } +
+ } } @else { diff --git a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html index b073d93a4e..fb82ed8032 100644 --- a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html @@ -23,21 +23,27 @@ (focusin)="onFocus()" [required]="required" [matAutocomplete]="messageTypeAutocomplete"> - + @if (messageTypeFormGroup.get('messageType').value && !disabled) { + + } - - - + @for (messageType of filteredMessageTypes | async; track messageType) { + + + + } - - {{ 'rulenode.message-type-required' | translate }} - + @if (messageTypeFormGroup.get('messageType').hasError('required')) { + + {{ 'rulenode.message-type-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/multiple-image-input.component.html b/ui-ngx/src/app/shared/components/multiple-image-input.component.html index a906430a7a..da82cf54db 100644 --- a/ui-ngx/src/app/shared/components/multiple-image-input.component.html +++ b/ui-ngx/src/app/shared/components/multiple-image-input.component.html @@ -16,7 +16,9 @@ -->
- + @if (label) { + + }
@@ -24,46 +26,58 @@ [dndDisableIf]="disabled" (dndDrop)="imageDrop($event)" [class.no-images]="!safeImageUrls.length">
-
- {{ 'image-input.images' | translate }} [{{ $index }}] -
-
- drag_indicator -
-
- -
-
- + @for (safeImageUrl of safeImageUrls; track safeImageUrl) { +
+ {{ 'image-input.images' | translate }} [{{ $index }}] +
+ @if (!disabled) { +
+ drag_indicator +
+ } +
+ +
+ @if (!disabled) { +
+ +
+ }
-
-
{{ 'image-input.no-images' | translate }}
+ } + @if (!safeImageUrls.length) { +
{{ 'image-input.no-images' | translate }}
+ }
-
-
- cloud_upload - image-input.drop-images-or - - + @if (!disabled) { +
+
+ cloud_upload + image-input.drop-images-or + + +
-
+ }
-
dashboard.maximum-upload-file-size
+ @if (maxSizeByte && !disabled) { +
dashboard.maximum-upload-file-size
+ }
diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.html b/ui-ngx/src/app/shared/components/notification/notification.component.html index 65e7c936f3..5a7a35eb2a 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.html +++ b/ui-ngx/src/app/shared/components/notification/notification.component.html @@ -17,35 +17,44 @@ -->
-
- - {{ notification.additionalConfig.icon.icon }} - -
- - - {{ notificationTypeIcons.get(notification.type) }} - - + @if (showIcon) { +
+ + {{ notification.additionalConfig.icon.icon }} + +
+ } @else { + @if (notificationTypeIcons.get(notification.type)) { + + {{ notificationTypeIcons.get(notification.type) }} + + } + }
- + @if (showButton) { + + }
{{ notification.createdTime ?? currentDate | dateAgo }} - -
- - {{alarmSeverityTranslations.get(notification.info.alarmSeverity) | translate}} - -
+ @if (!hideMarkAsReadButton) { + + } + @if (notification.type === notificationType.ALARM && !notification.info.cleared) { +
+ + {{alarmSeverityTranslations.get(notification.info.alarmSeverity) | translate}} + +
+ }
diff --git a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html index 9c2f5cade8..26ff960db8 100644 --- a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html @@ -23,53 +23,69 @@ (focusin)="onFocus()" [required]="required" [matAutocomplete]="templateAutocomplete"> - - - + @if (selectTemplateFormGroup.get('templateName').value && !disabled) { + + } + @if (selectTemplateFormGroup.get('templateName').value?.id && !disabled && allowEdit) { + + } + @if (allowCreate && !selectTemplateFormGroup.get('templateName').value && !disabled) { + + } - - - - - {{ notificationDeliveryMethodInfoMap.get(method.key).name | translate }} - - - - -
-
- notification.no-notification-templates + @for (template of (filteredTemplate | async); track template) { + + + + @for (method of template.configuration.deliveryMethodsTemplates | keyvalue; track method) { + + {{ getNotificationDeliveryMethodInfoMap(method.key).name | translate }} + + } + + + } + @if (!(filteredTemplate | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ notification.no-notification-templates +
+ } @else { + {{ 'notification.no-template-matching' | translate : {template: truncate.transform(searchText, true, 6,'...')} }} + @if (allowCreate) { + + notification.create-new-template + + } + }
- - {{ 'notification.no-template-matching' | translate : {template: truncate.transform(searchText, true, 6, '...')} }} - - notification.create-new-template - - -
- + + } - - {{ 'notification.template-required' | translate }} - + @if (selectTemplateFormGroup.get('templateName').hasError('required')) { + + {{ 'notification.template-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts index 6f3d6243b2..477f93d208 100644 --- a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts @@ -28,6 +28,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; import { emptyPageData } from '@shared/models/page/page-data'; import { + NotificationDeliveryMethod, NotificationDeliveryMethodInfoMap, NotificationTemplate, NotificationType @@ -268,4 +269,8 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn this.updateView(null); this.dirty = true; } + + getNotificationDeliveryMethodInfoMap(key: string) { + return this.notificationDeliveryMethodInfoMap.get(key as NotificationDeliveryMethod); + } } diff --git a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html index df1342fff4..7ff7f7ba2c 100644 --- a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html @@ -25,38 +25,51 @@ [matAutocomplete]="packageAutocomplete" [matAutocompleteDisabled]="disabled" [class.!hidden]="showDetailsPageLink && usePackageLink && disabled && otaPackageFormGroup.get('packageId').value"> - - {{ displayPackageFn(otaPackageFormGroup.get('packageId').value) }} - - + @if (showDetailsPageLink && usePackageLink && otaPackageFormGroup.get('packageId').value && disabled) { + + {{ displayPackageFn(otaPackageFormGroup.get('packageId').value) }} + + } + @if (otaPackageFormGroup.get('packageId').value && !disabled) { + + } - - - - -
-
- {{ notFoundPackage | translate }} -
- - + @for (package of filteredPackages | async; track package) { + + + + } + @if (!(filteredPackages | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ notFoundPackage | translate }} +
+ } @else { + {{ translate.get(notMatchingPackage, - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} - - -
-
+ {entity: truncate.transform(searchText, true, 6, '...')}) | async }} +
+ } +
+
+ }
- - {{ requiredErrorText | translate }} - - {{ hintText | translate }} + @if (otaPackageFormGroup.get('packageId').hasError('required')) { + + {{ requiredErrorText | translate }} + + } + @if (!disabled) { + {{ hintText | translate }} + } diff --git a/ui-ngx/src/app/shared/components/phone-input.component.html b/ui-ngx/src/app/shared/components/phone-input.component.html index 663576bd70..2590b5d913 100644 --- a/ui-ngx/src/app/shared/components/phone-input.component.html +++ b/ui-ngx/src/app/shared/components/phone-input.component.html @@ -17,16 +17,24 @@ -->
-
- {{ flagIcon }} - - - - {{country.flag}} - {{' ' + country.name + ' +' + country.dialCode }} - - -
+ @if (showFlagSelect) { +
+ @if (!isLoad) { + {{ flagIcon }} + } + @if (isLoad) { + + } + + @for (country of allCountries; track country) { + + {{country.flag}} + {{' ' + country.name + ' +' + country.dialCode }} + + } + +
+ } {{ label }} - - {{ requiredErrorText }} - - - {{ validationErrorText }} - + @if (phoneFormGroup.get('phoneNumber').hasError('required')) { + + {{ requiredErrorText }} + + } + @if (phoneFormGroup.get('phoneNumber').hasError('invalidPhoneNumber')) { + + {{ validationErrorText }} + + }
diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts index 7b74c90bfd..f971d94ec3 100644 --- a/ui-ngx/src/app/shared/components/popover.component.ts +++ b/ui-ngx/src/app/shared/components/popover.component.ts @@ -327,9 +327,9 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit { (overlayOutsideClick)="onClickOutside($event)" (detach)="hide()" (positionChange)="onPositionChange($event)" - > + >
+ (@popoverMotion.done)="animationDone()">
+ >
@@ -366,7 +370,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
- `, + `, standalone: false }) export class TbPopoverComponent implements OnDestroy, OnInit { diff --git a/ui-ngx/src/app/shared/components/protobuf-content.component.html b/ui-ngx/src/app/shared/components/protobuf-content.component.html index 167b68d2aa..ae485347d1 100644 --- a/ui-ngx/src/app/shared/components/protobuf-content.component.html +++ b/ui-ngx/src/app/shared/components/protobuf-content.component.html @@ -21,10 +21,12 @@
- + @if (!readonly && !disabled) { + + }
- + @if (selectQueueFormGroup.get('queueName').value && !disabled) { + + } - - - {{getDescription(queue)}} - - -
-
- queue.no-queues-found -
- - + @for (queue of filteredQueues | async; track queue) { + + + {{getDescription(queue)}} + + } + @if (!(filteredQueues | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ queue.no-queues-found +
+ } @else { + {{ translate.get('queue.no-queues-matching', - {queue: truncate.transform(searchText, true, 6, '...')}) | async }} - - -
-
+ {queue: truncate.transform(searchText, true, 6, '...')}) | async }} +
+ } +
+
+ }
- {{ autocompleteHint | translate }} - - {{ 'queue.queue-required' | translate }} - + @if (autocompleteHint) { + {{ autocompleteHint | translate }} + } + @if (selectQueueFormGroup.get('queueName').hasError('required')) { + + {{ 'queue.queue-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html index 8e4542bad2..397c58dc78 100644 --- a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html @@ -17,7 +17,9 @@ --> - {{ 'relation.relation-type' | translate }} + @if (showLabel) { + {{ 'relation.relation-type' | translate }} + } - + @if (relationTypeFormGroup.get('relationType').value && !disabled) { + + } - - - + @for (relationType of filteredRelationTypes | async; track relationType) { + + + + } - - {{ 'relation.relation-type-required' | translate }} - - - {{ 'relation.relation-type-max-length' | translate }} - + @if (relationTypeFormGroup.get('relationType').hasError('required')) { + + {{ 'relation.relation-type-required' | translate }} + + } + @if (relationTypeFormGroup.get('relationType').hasError('maxlength')) { + + {{ 'relation.relation-type-max-length' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html b/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html index 5f3160168e..f461ad16cb 100644 --- a/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html @@ -29,36 +29,44 @@ [required]="required" [matAutocomplete]="entityAutocomplete" [matAutocompleteDisabled]="!allowAutocomplete"> - + @if (resourceFormGroup.get('resource').value && !disabled) { + + } - - - - - -
-
- {{ 'js-func.no-js-module-text' | translate }} -
- - - {{ 'js-func.no-js-module-matching' | translate: {module: searchText | truncate: true: 6: '...'} }} - - -
-
-
- - - {{ searchText }} + @for (resource of filteredResources$ | async; track resource) { + + - + } + @if (subType !== ResourceSubType.EXTENSION) { + @if (!(filteredResources$ | async)?.length) { + +
+ @if (!textIsNotEmpty(searchText)) { +
+ {{ 'js-func.no-js-module-text' | translate }} +
+ } @else { + + {{ 'js-func.no-js-module-matching' | translate: {module: searchText | truncate: true: 6: '...'} }} + + } +
+
+ } + } @else { + @if (!(filteredResources$ | async)?.length) { + + {{ searchText }} + + } + }
diff --git a/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html index 5b357b3ed0..253a92e50a 100644 --- a/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html +++ b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html @@ -18,7 +18,7 @@

{{configuration.title}}

- + @if (data.multiple) {
@@ -35,20 +35,21 @@ - - - - {{ image.title }} - - + @if (displayPreview) { + + + + {{ image.title }} + + + } - + @if (dataSource.selection.isEmpty()) { {{ 'resource.title' | translate }} - - + } @else { {{ translate.get(configuration.selectedText, {count: dataSource.selection.selected.length}) | async }} - + } {{ resource.title }} @@ -60,23 +61,22 @@ + (click)="toggleShowReferences($event, resource, showReferencesButton)">{{ 'image.references' | translate }} + *matRowDef="let resource; columns: configuration.columns;">
-
+ } @else { + +
+ }
+ cdkFocusInitial>{{ (data.multiple ? 'action.delete-selected' : 'action.delete-anyway') | translate}}
- - -
-
diff --git a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html index beb6f09081..62584f5336 100644 --- a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html +++ b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html @@ -25,19 +25,25 @@ - - check - - - -
- - {{ 'rulechain.no-rulechains-matching' | translate : {entity: searchText} }} - -
-
+ @for (ruleChain of filteredRuleChains | async; track ruleChain) { + + @if (ruleChainId === ruleChain.id.id) { + check + } + + + } + @if (!(filteredRuleChains | async)?.length) { + +
+ + {{ 'rulechain.no-rulechains-matching' | translate : {entity: searchText} }} + +
+
+ }
diff --git a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html index 3db3d06147..a0d3c4023a 100644 --- a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.html @@ -23,30 +23,39 @@ (focusin)="onFocus()" [required]="required" [matAutocomplete]="slackAutocomplete"> - + @if (conversationSlackFormGroup.get('conversation').value && !disabled) { + + } - - - - -
-
- notification.not-found-slack-recipient + @for (template of slackConversation$ | async; track template) { + + + + } + @if (!(slackConversation$ | async)?.length) { + +
+ @if (!textIsNotEmpty(slackSearchText)) { +
+ notification.not-found-slack-recipient +
+ } @else { + Not slack recipient matches {{ truncate.transform(slackSearchText, true, 6, '...') }} + }
- - Not slack recipient matches {{ truncate.transform(slackSearchText, true, 6, '...') }} - -
- + + } - - {{ requiredText }} - + @if (conversationSlackFormGroup.get('conversation').hasError('required')) { + + {{ requiredText }} + + } diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.html b/ui-ngx/src/app/shared/components/snack-bar-component.html index 2197cebdcc..0bd630d154 100644 --- a/ui-ngx/src/app/shared/components/snack-bar-component.html +++ b/ui-ngx/src/app/shared/components/snack-bar-component.html @@ -24,11 +24,10 @@ [class.warn-toast]="notification.type === 'warn'" [class.success-toast]="notification.type === 'success'" [class.info-toast]="notification.type === 'info'"> - + @if (!notification.modern) {
-
- + } @else {
- + }
diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.html b/ui-ngx/src/app/shared/components/string-autocomplete.component.html index 8891fad40f..2471b5caf3 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.html @@ -17,35 +17,45 @@ --> - {{label}} + @if (label) { + {{label}} + } - - - warning - - - {{ getErrorMessage }} - + @if (selectionFormControl.value && !disabled) { + + } + @if (selectionFormControl.errors && selectionFormControl.touched && !showInlineError) { + + warning + + } + @if (selectionFormControl.errors && showInlineError) { + + {{ getErrorMessage }} + + } - - - + @for (option of filteredOptions$ | async; track option) { + + + + } diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index 106b79ab9a..c6b5b1f481 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -20,16 +20,22 @@ [floatLabel]="floatLabel" [appearance]="appearance" [subscriptSizing]="subscriptSizing"> - {{ label }} + @if (label) { + {{ label }} + } - - {{ item.name }} - close - + @for (item of itemList; track item) { + + {{ item.name }} + @if (!disabled) { + close + } + + } @for (value of filteredValues | async; track value.value) { - } @empty { + } @empty { @if (!allowUserValue) { {{ 'common.not-found' | translate }} } @@ -66,7 +72,9 @@
- - {{ requiredText }} - + @if (stringItemsForm.get('items').hasError('required')) { + + {{ requiredText }} + + } diff --git a/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.html b/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.html index ef2dec83fe..1f10bc4974 100644 --- a/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/string-pattern-autocomplete.component.html @@ -17,7 +17,9 @@ --> - {{label}} + @if (label) { + {{label}} + }

   mdi:variable
     
   }
-  
-    warning
-  
-  
-    {{errorText}}
-  
+  @if (selectionFormControl.hasError('required') && !showInlineError) {
+    
+      warning
+    
+  }
+  @if (selectionFormControl.hasError('required') && showInlineError) {
+    
+      {{errorText}}
+    
+  }
 
diff --git a/ui-ngx/src/app/shared/components/svg-xml.component.html b/ui-ngx/src/app/shared/components/svg-xml.component.html index 55ba188547..a720568c47 100644 --- a/ui-ngx/src/app/shared/components/svg-xml.component.html +++ b/ui-ngx/src/app/shared/components/svg-xml.component.html @@ -19,7 +19,9 @@ tb-fullscreen [fullscreen]="fullscreen">
- + @if (!noLabel) { + + }
@if (inlineField) { - - warning - + @if (hasError) { + + warning + + } + } + @if ((subscriptSizing === 'fixed') && !inlineField) { + {{ hintText }} + } + @if (hasError && !inlineField) { + + {{ hasError }} + } - {{ hintText }} - - {{ hasError }} -
- - {{ aggregationTypesTranslations.get(aggregationTypes[type]) | translate }} - + @for (type of allAggregationTypes; track type) { + + {{ aggregationTypesTranslations.get(aggregationTypes[type]) | translate }} + + }
diff --git a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html index a63ee7540b..bd2a4aee7d 100644 --- a/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html +++ b/ui-ngx/src/app/shared/components/time/aggregation/aggregation-type-select.component.html @@ -18,11 +18,15 @@ - {{ label }} + @if (displayLabel) { + {{ label }} + } - - {{ displayAggregationTypeFn(type) }} - + @for (type of aggregationTypes; track type) { + + {{ displayAggregationTypeFn(type) }} + + } diff --git a/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html b/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html index 75e53e4404..f3c474d2a4 100644 --- a/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html +++ b/ui-ngx/src/app/shared/components/time/aggregation/grouping-interval-options.component.html @@ -20,12 +20,16 @@ [subscriptSizing]="subscriptSizing" [appearance]="appearance"> - - {{ 'timewindow.all' | translate }} - - - {{ interval.name | translate:interval.translateParams }} - + @if (timeintervalFormGroup.get('aggIntervals').value?.length === allIntervals.length) { + + {{ 'timewindow.all' | translate }} + + } + @for (interval of allIntervals; track interval) { + + {{ interval.name | translate:interval.translateParams }} + + } -- - - {{ interval.name | translate:interval.translateParams }} - + @for (interval of selectedIntervals; track interval) { + + {{ interval.name | translate:interval.translateParams }} + + } diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.html b/ui-ngx/src/app/shared/components/time/datetime.component.html index 8ef3ee3c21..25861aa4d4 100644 --- a/ui-ngx/src/app/shared/components/time/datetime.component.html +++ b/ui-ngx/src/app/shared/components/time/datetime.component.html @@ -21,14 +21,18 @@ [class.tb-time-field]="!fieldClass && type === 'time'" [class.with-clear]="allowClear" [subscriptSizing]="subscriptSizing" [appearance]="appearance" [floatLabel]="floatLabel"> - {{ dateText }} + @if (showLabel) { + {{ dateText }} + }
- + @if (allowClear && date && !disabled) { + + }
diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html index c7c983c371..152f4ca083 100644 --- a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html @@ -16,39 +16,49 @@ -->
-
- - -
- - -
- {{ currentTime | date:'medium'}} - {{ "widget.no-data-found" | translate}} -
-
- - - - - - {{speedValue}} - +
+ + +
+ + +
+ @if (currentTime) { + {{ currentTime | date:'medium'}} + } + @if (!currentTime) { + {{ "widget.no-data-found" | translate}} + } +
+ + + @if (!playing) { + + } + @if (playing) { + + } + + @for (speedValue of speeds; track speedValue) { + {{speedValue}} + } + +
diff --git a/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html b/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html index 8953db42b1..646c63df94 100644 --- a/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/interval-options-config-panel.component.html @@ -32,20 +32,20 @@ {{ "timewindow.interval" | translate }}
- + @if (aggregation) {
{{"timewindow.allowed-agg-intervals" | translate }}
{{"timewindow.default-agg-interval" | translate }}
-
+ }
- + @for (interval of intervalsFormArray.controls; track trackByElement(i, interval); let i = $index) {
{{ interval.get('name').value }}
- + @if (aggregation) {
-
+ }
-
+ }
diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html index 66bbb01b14..0b95467753 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html @@ -17,11 +17,15 @@ -->
- timewindow.interval + @if (displayLabel) { + timewindow.interval + } - - {{ timeIntervalTranslationMap.get(interval) | translate}} - + @for (interval of intervals; track interval) { + + {{ timeIntervalTranslationMap.get(interval) | translate}} + + } diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.html b/ui-ngx/src/app/shared/components/time/timeinterval.component.html index cc9793155c..b0955afcc3 100644 --- a/ui-ngx/src/app/shared/components/time/timeinterval.component.html +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.html @@ -19,11 +19,15 @@
- {{ predefinedName }} + @if (predefinedName) { + {{ predefinedName }} + } - - {{ interval.name | translate:interval.translateParams }} - + @for (interval of intervals; track interval) { + + {{ interval.name | translate:interval.translateParams }} + + } diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html index c3aee54ac9..272fa6239c 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html @@ -19,7 +19,7 @@

{{ 'timewindow.timewindow-settings' | translate }}

-
+
-
-
{{ 'timewindow.timewindow' | translate }}
-
- - {{ 'timewindow.hide-timewindow-section' | translate }} - -
- - - - - + @if (timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME) { +
+
{{ 'timewindow.timewindow' | translate }}
- - {{ 'timewindow.disable-custom-interval' | translate }} + + {{ 'timewindow.hide-timewindow-section' | translate }}
-
- -
- {{ 'timewindow.hide' | translate }} -
-
- + + } + @if (timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL) { +
+ + {{ 'timewindow.disable-custom-interval' | translate }} + +
+
+ @if (!quickIntervalOnly) { + +
+ {{ 'timewindow.hide' | translate }} +
+
+ } + - - -
- - -
- -
- {{ 'timewindow.hide' | translate }} + @if (!(timewindowForm.get('realtime.hideLastInterval').value || + timewindowForm.get('realtime.hideInterval').value)) { + + } +
-
- + @if (!quickIntervalOnly) { + +
+ {{ 'timewindow.hide' | translate }} +
+
+ } + - - -
-
- -
-
{{ 'timewindow.timewindow' | translate }}
-
- - {{ 'timewindow.hide-timewindow-section' | translate }} - -
- - - + edit + + } + +
+ } + + } - + @if (timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY) { +
+
{{ 'timewindow.timewindow' | translate }}
- - {{ 'timewindow.disable-custom-interval' | translate }} + + {{ 'timewindow.hide-timewindow-section' | translate }}
-
- -
- {{ 'timewindow.hide' | translate }} -
-
- + + @if (timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL) { +
+ + {{ 'timewindow.disable-custom-interval' | translate }} + +
+
+ +
+ {{ 'timewindow.hide' | translate }} +
+
+ - - -
- - -
- -
- {{ 'timewindow.hide' | translate }} + @if (!(timewindowForm.get('history.hideLastInterval').value || + timewindowForm.get('history.hideInterval').value)) { + + } +
-
- + +
+ {{ 'timewindow.hide' | translate }} +
+
+ - -
- -
- -
- {{ 'timewindow.hide' | translate }} -
-
- +
+ } + @if (timewindowForm.get('history.historyType').value === historyTypes.INTERVAL) { +
+ +
+ {{ 'timewindow.hide' | translate }} +
+
+ - - -
-
+ @if (!(timewindowForm.get('history.hideQuickInterval').value || + timewindowForm.get('history.hideInterval').value)) { + + } + +
+ } + + } - + @if (aggregation) {
{{ 'aggregation.aggregation' | translate }}
@@ -198,37 +215,38 @@ - + @if (!timewindowForm.get('hideAggregation').value) { + + }
- -
-
{{ 'aggregation.limit' | translate }}
-
- -
- {{ 'timewindow.hide' | translate }} -
-
- - - - -
-
- + @if (timewindowForm.get('aggregation.type').value === aggregationTypes.NONE) { +
+
{{ 'aggregation.limit' | translate }}
+
+ +
+ {{ 'timewindow.hide' | translate }} +
+
+ + + + +
+
+ }
{{ 'aggregation.group-interval' | translate }}
- - + @if (timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME) {
{{ 'timewindow.disable-custom-interval' | translate }} @@ -250,8 +268,8 @@
-
- + } + @if (timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY) {
{{ 'timewindow.disable-custom-interval' | translate }} @@ -273,8 +291,7 @@
-
- + }
@@ -283,7 +300,7 @@
-
+ }
{{ 'timezone.timezone' | translate }}
diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index 6a6ca10734..322da81efb 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -16,163 +16,194 @@ -->
-
- - - -
+ @if (!historyOnly) { +
+ + + @if (isEdit) { + + } +
+ }
- -
-
-
-
{{ 'timewindow.timewindow' | translate }}
- - - + @if (timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME) { + @if (realtimeIntervalSelectionAvailable) { +
+
+
+
{{ 'timewindow.timewindow' | translate }}
+ @if (realtimeTypeSelectionAvailable) { + + + + + } +
+
- - - -
- -
- + @if (timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL) { + - - - + } + @if (timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL) { + - -
-
-
- - -
-
-
-
{{ 'timewindow.timewindow' | translate }}
- - - - + + }
- +
+ } @else { +
+
{{ 'timezone.timezone' | translate }}
-
+
+ } + } -
- +
+
+
{{ 'timewindow.timewindow' | translate }}
+ @if (historyTypeSelectionAvailable) { + + + + + } +
+ + +
+ @if (historyIntervalSelectionAvailable && + timewindowForm.get('history.historyType').value !== historyTypes.FOR_ALL_TIME) { +
+ @if (timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL) { + - - - + } + @if (timewindowForm.get('history.historyType').value === historyTypes.FIXED) { + - - - + } + @if (timewindowForm.get('history.historyType').value === historyTypes.INTERVAL) { + - -
- - - - - -
-
{{ 'aggregation.aggregation' | translate }}
- - + + } +
+ } - -
-
{{ 'aggregation.limit' | translate }}
- - + } @else { +
+
{{ 'timezone.timezone' | translate }}
+ +
- + } + } -
- - -
{{ 'aggregation.group-interval' | translate }}
-
-
- - -
{{ 'aggregation.group-interval' | translate }}
-
-
-
- + @if (aggregationOptionsAvailable) { + + @if (isEdit || !timewindow.hideAggregation) { +
+
{{ 'aggregation.aggregation' | translate }}
+ + +
+ } + @if (timewindowForm.get('aggregation.type').value === aggregationTypes.NONE && (isEdit || !timewindow.hideAggInterval)) { +
+
{{ 'aggregation.limit' | translate }}
+ + +
+ } +
+ @if (timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE) { +
+ @if (timewindowForm.get('selectedTab').value === timewindowTypes.REALTIME) { + + +
{{ 'aggregation.group-interval' | translate }}
+
+
+ } + @if (timewindowForm.get('selectedTab').value === timewindowTypes.HISTORY) { + + +
{{ 'aggregation.group-interval' | translate }}
+
+
+ } +
+ } + } @if (saveAsDefaultAvailable) { {{ 'timewindow.save-current-settings-as-default' | translate }} @@ -180,34 +211,33 @@ }
- -
-
{{ 'timezone.timezone' | translate }}
- - -
-
- - + @if (timezone && (isEdit || !timewindow.hideTimezone)) { + + + } - -
- - -
+ @if (panelMode) { + + } + @if (panelMode) { +
+ + +
+ } diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.html b/ui-ngx/src/app/shared/components/time/timewindow.component.html index 55c2f58120..5a2d5016a0 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.html @@ -15,44 +15,56 @@ limitations under the License. --> - - - -
- {{ computedTimewindowStyle.icon }} -
- {{innerValue?.displayValue}} | {{innerValue?.displayTimezoneAbbr}} -
- {{ computedTimewindowStyle.icon }} -
+@if (asButton && panelMode && !strokedButton && !flatButton) { + +} +@if (asButton && panelMode && strokedButton) { + +} +@if (asButton && panelMode && flatButton) { + +} +@if (!asButton && panelMode) { +
+ @if (computedTimewindowStyle.showIcon && computedTimewindowStyle.iconPosition === 'left') { + {{ computedTimewindowStyle.icon }} + } +
+ {{innerValue?.displayValue}} | {{innerValue?.displayTimezoneAbbr}} +
+ @if (computedTimewindowStyle.showIcon && computedTimewindowStyle.iconPosition === 'right') { + {{ computedTimewindowStyle.icon }} + } +
+} diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.html b/ui-ngx/src/app/shared/components/time/timezone-select.component.html index 73343b71e3..5e343ce946 100644 --- a/ui-ngx/src/app/shared/components/time/timezone-select.component.html +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.html @@ -17,35 +17,45 @@ --> - timezone.timezone + @if (displayLabel) { + timezone.timezone + } - + @if (selectTimezoneFormGroup.get('timezone').value && !disabled) { + + } - - - - - + @for (timezone of filteredTimezones | async; track timezone) { + + + + } + @if (!(filteredTimezones | async)?.length) { + + {{ translate.get('timezone.no-timezones-matching', {timezone: searchText}) | async }} - - + + + } - - {{ 'timezone.timezone-required' | translate }} - + @if (selectTimezoneFormGroup.get('timezone').hasError('required')) { + + {{ 'timezone.timezone-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/time/timezone.component.html b/ui-ngx/src/app/shared/components/time/timezone.component.html index 1ca6c746b8..3f37bcdec9 100644 --- a/ui-ngx/src/app/shared/components/time/timezone.component.html +++ b/ui-ngx/src/app/shared/components/time/timezone.component.html @@ -15,40 +15,48 @@ limitations under the License. --> - - - -
-
- {{displayValue()}} -
-
+@if (asButton && !strokedButton && !flatButton) { + +} +@if (asButton && strokedButton) { + +} +@if (asButton && flatButton) { + +} +@if (!asButton) { +
+
+ {{displayValue()}} +
+
+} diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html index bddaa896b6..e9063b3be2 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -26,33 +26,48 @@ [class.tb-mat-24]="isMdLg"> chevron_left -
- - - {{ option.name }} - - warning - - - -
+@if ((useSelect$ | async) === false) { +
+ + @for (option of options; track trackByHeaderOption($index, option)) { + + {{ option.name }} + @if ((option.error$ | async) && value !== option.value) { + + warning + + } + + } + +
+} @else { + + + @for (option of options; track option) { + {{ option.name }} + } + + +} - - - - {{ option.name }} - - - diff --git a/ui-ngx/src/app/shared/components/unit-input.component.html b/ui-ngx/src/app/shared/components/unit-input.component.html index 66d92c83d1..c2cd671072 100644 --- a/ui-ngx/src/app/shared/components/unit-input.component.html +++ b/ui-ngx/src/app/shared/components/unit-input.component.html @@ -23,27 +23,33 @@ (focusin)="onFocus()" [matAutocomplete]="unitsAutocomplete" [matAutocompleteDisabled]="supportsUnitConversion"> - - - warning - - - mdi:swap-vertical-circle-outline - + @if (unitsFormControl.value && !disabled && unitsFormControl.valid && !isUnitMapping) { + + } + @if (unitsFormControl.hasError('required')) { + + warning + + } + @if (supportsUnitConversion && isUnitMapping) { + + mdi:swap-vertical-circle-outline + + } } - } + } diff --git a/ui-ngx/src/app/shared/components/unit-settings-panel.component.html b/ui-ngx/src/app/shared/components/unit-settings-panel.component.html index 41875971f8..c27f56dd3f 100644 --- a/ui-ngx/src/app/shared/components/unit-settings-panel.component.html +++ b/ui-ngx/src/app/shared/components/unit-settings-panel.component.html @@ -22,9 +22,9 @@
unit.unit-settings.source-unit
+ translate>unit.unit-settings.source-unit
+ [measure]="measure" [tagFilter]="tagFilter">
@@ -36,7 +36,7 @@
unit.unit-settings.target-metric-unit
+ translate>unit.unit-settings.target-metric-unit
unit.unit-settings.target-imperial-unit
+ translate>unit.unit-settings.target-imperial-unit
unit.unit-settings.target-hybrid-unit
+ translate>unit.unit-settings.target-hybrid-unit
- + @if (!disabled) { + + } - + @if (!disabled) { + + }
diff --git a/ui-ngx/src/app/shared/components/user-menu.component.html b/ui-ngx/src/app/shared/components/user-menu.component.html index 30b67fd38c..91454db185 100644 --- a/ui-ngx/src/app/shared/components/user-menu.component.html +++ b/ui-ngx/src/app/shared/components/user-menu.component.html @@ -16,26 +16,34 @@ -->
-
diff --git a/ui-ngx/src/app/shared/components/value-input.component.html b/ui-ngx/src/app/shared/components/value-input.component.html index a2f9426aa3..1ee5ac89b3 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.html +++ b/ui-ngx/src/app/shared/components/value-input.component.html @@ -17,85 +17,107 @@ -->
- - - -
- - {{ valueTypes.get(valueType).name | translate }} -
-
- - - {{ valueTypes.get(valueTypeEnum[valueType]).name | translate }} - -
-
- - - - warning - - - - - + + +
+ + {{ valueTypes.get(valueType).name | translate }} +
+
+ @for (valueType of valueTypeKeys; track valueType) { + + + {{ valueTypes.get(valueTypeEnum[valueType]).name | translate }} + + } +
+
+ } + @if (valueType === valueTypeEnum.STRING) { + + + @if (value.hasError('required') && value.control.touched) { + + warning + + } + + } + @if (valueType === valueTypeEnum.INTEGER) { + + + @if (value.control.invalid && value.control.touched) { + - warning - - - - - - warning - - - - - {{ trueLabel }} - {{ falseLabel }} - - - {{ modelValue ? trueLabel : falseLabel }} - - -
- - - + warning + + } + + } + @if (valueType === valueTypeEnum.DOUBLE) { + + + @if (value.hasError('required') && value.control.touched) { + + warning + + } + + } + @if (valueType === valueTypeEnum.BOOLEAN) { + @if (!shortBooleanField) { + + {{ trueLabel }} + {{ falseLabel }} + + } + @if (shortBooleanField) { + + {{ modelValue ? trueLabel : falseLabel }} + + } + } + @if (valueType === valueTypeEnum.JSON) { +
+ + + @if (value.control.invalid && value.control.touched) { + - warning - - - -
+ class="tb-error"> + warning + + } + + @if (!hideJsonEdit) { + + } +
+ }
diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html index 26df6befba..40ddca3dce 100644 --- a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html @@ -29,25 +29,37 @@ (blur)="onBlur()" [required]="required" [matAutocomplete]="branchAutocomplete"> - + @if (branchFormGroup.get('branch').value && !disabled) { + + } - - check - - - {{ 'version-control.default' | translate }} - + @for (branch of filteredBranches | async; track branch) { + + @if (selectionMode && branch.name === modelValue) { + check + } + @if (selectionMode && branch.name !== modelValue) { + + } + + @if (branch.default) { + {{ 'version-control.default' | translate }} + } + + } - - {{ 'version-control.branch-required' | translate }} - + @if (branchFormGroup.get('branch').hasError('required')) { + + {{ 'version-control.branch-required' | translate }} + + } diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html b/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html index 8332d0a268..270284dc11 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html +++ b/ui-ngx/src/app/shared/components/widgets-bundle-search.component.html @@ -23,7 +23,9 @@ (focus)="toggleFocus()" (blur)="toggleFocus()" [placeholder]=placeholder> - + @if (searchText) { + + }
diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html index 04355f2bc1..5a69f8ddc3 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html @@ -27,20 +27,29 @@
{{widgetsBundle?.title}} - widgets-bundle.system + @if (isSystem(widgetsBundle)) { + widgets-bundle.system + }
- - widgets-bundle.create-new - - - - - -
- {{widgetsBundle.title}} - widgets-bundle.system -
-
+ @if (createNew) { + + widgets-bundle.create-new + + } @else { + @if (!required) { + + } + } + @for (widgetsBundle of widgetsBundles$ | async; track widgetsBundle) { + +
+ {{widgetsBundle.title}} + @if (isSystem(widgetsBundle)) { + widgets-bundle.system + } +
+
+ } diff --git a/ui-ngx/src/app/shared/import-export/export-resource-dialog.component.html b/ui-ngx/src/app/shared/import-export/export-resource-dialog.component.html index ef27998db3..7bd900f1cf 100644 --- a/ui-ngx/src/app/shared/import-export/export-resource-dialog.component.html +++ b/ui-ngx/src/app/shared/import-export/export-resource-dialog.component.html @@ -24,8 +24,10 @@ close - - +@if ((isLoading$ | async) && !ignoreLoading) { + + +}
{{ prompt | translate }} diff --git a/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html index eec9ee4e39..d2cd853e02 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html +++ b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html @@ -27,8 +27,10 @@ close - - + @if (isLoading$ | async) { + + + }
@@ -47,11 +49,11 @@
+ (click)="cancel()">{{ 'action.cancel' | translate }} + (click)="nextStep(2)">{{ 'action.continue' | translate }}
@@ -61,9 +63,11 @@ import.csv-delimiter - - {{ delimiter.value }} - + @for (delimiter of delimiters; track delimiter) { + + {{ delimiter.value }} + + }
@@ -79,15 +83,15 @@
+ (click)="previousStep()">{{ 'action.back' | translate }}
+ (click)="cancel()">{{ 'action.cancel' | translate }} + (click)="nextStep(3)">{{ 'action.continue' | translate }}
@@ -99,15 +103,15 @@
+ (click)="previousStep()">{{ 'action.back' | translate }}
+ (click)="cancel()">{{ 'action.cancel' | translate }} + (click)="nextStep(4)">{{ 'action.continue' | translate }}
@@ -119,32 +123,39 @@ {{ 'action.done' | translate }}
-

- {{ translate.instant('import.message.create-entities', {count: statistical.created}) }} -

-

- {{ translate.instant('import.message.update-entities', {count: statistical.updated}) }} -

-

- {{ translate.instant('import.message.error-entities', {count: statistical.errors}) }} -

- - - -
import.details
-
-
- -
-
+ @if (statistical?.created) { +

+ {{ translate.instant('import.message.create-entities', {count: statistical.created}) }} +

+ } + @if (statistical?.updated) { +

+ {{ translate.instant('import.message.update-entities', {count: statistical.updated}) }} +

+ } + @if (statistical?.errors) { +

+ {{ translate.instant('import.message.error-entities', {count: statistical.errors}) }} +

+ } + @if (statistical?.errorsList?.length) { + + + +
import.details
+
+
+ +
+
+ }
+ (click)="nextStep(6)">{{ 'action.ok' | translate }}
diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.html b/ui-ngx/src/app/shared/import-export/import-dialog.component.html index af672e4281..ec6ed1ff6b 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.html +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.html @@ -25,32 +25,40 @@ close - - + @if (isLoading$ | async) { + + + }
- - {{ importFileLabel | translate }} - {{ importContentLabel | translate }} - - - - - + @if (enableImportFromContent) { + + {{ importFileLabel | translate }} + {{ importContentLabel | translate }} + + } + @if (importFormGroup.get('importType').value === 'file') { + + + } + @if (importFormGroup.get('importType').value === 'content') { + + + }
diff --git a/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html index 46be8d8c57..10fa39c547 100644 --- a/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html +++ b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html @@ -32,25 +32,33 @@ {{ 'import.column-type.column-type' | translate }} - - {{ columnTypesTranslations.get(type.value) | translate }} - - - - {{ columnTypesTranslations.get(credential.value) | translate }} + @for (type of columnTypes; track type) { + + {{ columnTypesTranslations.get(type.value) | translate }} - + } + @if (entityType === entityTypeDevice) { + + @for (credential of columnDeviceCredentials; track credential) { + + {{ columnTypesTranslations.get(credential.value) | translate }} + + } + + } {{ 'import.column-key' | translate }} - - - + @if (isColumnTypeDiffers(column.type)) { + + + + } diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 190f486536..721b8b8006 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -238,18 +238,6 @@ "@angular-devkit/architect" "0.2003.20" rxjs "7.8.2" -"@angular-devkit/core@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.18.tgz#a079775ba6a31583a0d57813b374a6c8c997f252" - integrity sha512-zGWMjMqE8qXYr8baYCs43k9HlKz9J4Gh3Yx+7XE0uS0Y1LXzzALevSoUw7GIPdSvOriQJAEgtWE6QKssqSGltQ== - dependencies: - ajv "8.18.0" - ajv-formats "3.0.1" - jsonc-parser "3.3.1" - picomatch "4.0.3" - rxjs "7.8.2" - source-map "0.7.6" - "@angular-devkit/core@20.3.20", "@angular-devkit/core@>= 20.0.0 < 21.0.0", "@angular-devkit/core@^20.0.0": version "20.3.20" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.20.tgz#62278795afb05ff1c2f442387e5c3e996ac304bf" From 2a54290db1aabfd3237afb74629c6345c0775a35 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 14:08:40 +0200 Subject: [PATCH 197/313] Mark form dirty when OR is force-reset to AND When OR is disabled but a filter had OR saved, the reset to AND now marks the form dirty so the Update button is enabled and the user can save the corrected value. --- .../home/components/filter/filter-dialog.component.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts index b77a951984..10c3941ee6 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts @@ -94,18 +94,20 @@ export class FilterDialogComponent extends DialogComponent { if (value === ComplexOperation.OR) { - this.filterFormGroup.get('keyFiltersOperation').setValue(ComplexOperation.AND, {emitEvent: false}); + this.filterFormGroup.get('keyFiltersOperation').setValue(ComplexOperation.AND); } }); } From 2c639d534b3f0b6c98f6e4893f5cc2581a56f9c5 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 14:12:57 +0200 Subject: [PATCH 198/313] Fix toggle disabled behavior via form control API Disable the keyFiltersOperation form control when OR is not allowed. This uses Angular's reactive form disabled state which properly propagates through setDisabledState to tb-toggle-select, greying out the entire toggle. If the filter had OR saved, it resets to AND and marks the form dirty so Update is enabled. The tooltip on hover explains why the toggle is disabled. --- .../components/filter/filter-dialog.component.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts index 10c3941ee6..334ce90931 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts @@ -100,16 +100,12 @@ export class FilterDialogComponent extends DialogComponent { - if (value === ComplexOperation.OR) { - this.filterFormGroup.get('keyFiltersOperation').setValue(ComplexOperation.AND); - } - }); + if (this.filter.keyFiltersOperation === ComplexOperation.OR) { + this.filterFormGroup.get('keyFiltersOperation').setValue(ComplexOperation.AND); + this.filterFormGroup.markAsDirty(); + } + this.filterFormGroup.get('keyFiltersOperation').disable(); } } From 46853ef7b8fc6f2f633a4e6490946f543a608bf7 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 24 Mar 2026 15:12:46 +0200 Subject: [PATCH 199/313] Address PR review comments and add missing test coverage - Wrap entityKeyColumn null-swap in try/finally in EntityKeyMapping - Remove @Setter from BaseEntityService, use ReflectionTestUtils in test - Replace Thread.sleep with awaitility in all OR test methods - Fix confusing temperature comment in pagination test - Add test for OR with textSearch (middle-layer WHERE logic) - Add test for OR with TIME_SERIES key filters - Add test for OR with ComplexFilterPredicate within key filter - Add EDQS tests for STRING/entity-field with OR (including null special case) --- .../controller/EntityQueryControllerTest.java | 273 +++++++++++++++--- .../server/dao/entity/BaseEntityService.java | 2 - .../dao/sql/query/EntityKeyMapping.java | 15 +- .../server/edqs/repo/RepositoryUtilsTest.java | 45 +++ 4 files changed, 288 insertions(+), 47 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index d8081f60dd..46ebf31f9d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -28,6 +28,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; @@ -62,6 +63,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo; +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.ComplexOperation; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DynamicValue; @@ -94,7 +96,6 @@ import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.edqs.util.EdqsRocksDb; -import org.thingsboard.server.service.query.DefaultEntityQueryService; import java.util.ArrayList; import java.util.Arrays; @@ -127,8 +128,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { @Autowired private QueueStatsService queueStatsService; @Autowired - private DefaultEntityQueryService entityQueryService; - @Autowired private BaseEntityService baseEntityService; @MockitoBean @@ -1762,8 +1761,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { String payloadC = "{\"temperature\":30}"; doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, payloadC, String.class, status().isOk()); - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("orTestType")); filter.setDeviceNameFilter(""); @@ -1790,7 +1787,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // OR: deviceA (60>50) and deviceB (5<10) match => count=2 EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); - countByQueryAndCheck(orQuery, 2); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); // AND: no device has temperature both >50 AND <10 => count=0 EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); @@ -1820,8 +1817,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { deviceZ = doPost("/api/device", deviceZ, Device.class); // deviceZ has neither matching attribute - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("orDataType")); filter.setDeviceNameFilter(""); @@ -1854,6 +1849,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // OR: deviceX matches status=active, deviceY matches humidity>70 EntityDataQuery orQuery = new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery, 2)); PageData result = findByQueryAndCheck(orQuery, 2); List names = result.getData().stream() .map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()) @@ -1878,8 +1874,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"temperature\":5}", String.class, status().isOk()); - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("orSameKeyType")); filter.setDeviceNameFilter(""); @@ -1912,6 +1906,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // OR on same key: deviceA (60>50) and deviceB (5<10) should both be returned EntityDataQuery orQuery = new EntityDataQuery(filter, pageLink, entityFields, null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery, 2)); PageData result = findByQueryAndCheck(orQuery, 2); List names = result.getData().stream() .map(e -> e.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()) @@ -1936,8 +1931,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"temperature\":5}", String.class, status().isOk()); - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("backCompatType")); filter.setDeviceNameFilter(""); @@ -1962,6 +1955,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List keyFilters = List.of(tempGt50, tempLt10); + // Await attribute propagation: verify with an OR query that should find 2 when propagated + EntityCountQuery orCheckQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orCheckQuery, 2)); + // Query without keyFiltersOperation (null) -- should behave as AND EntityCountQuery nullOpQuery = new EntityCountQuery(filter, keyFilters); Long nullResult = countByQueryAndCheck(nullOpQuery, 0); @@ -1999,8 +1996,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { doPost("/api/plugins/telemetry/" + deviceMid.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"temperature\":30}", String.class, status().isOk()); - Thread.sleep(500); - // Create alarms for each device Alarm alarmHot = new Alarm(); alarmHot.setOriginator(deviceHot.getId()); @@ -2020,8 +2015,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { alarmMid.setSeverity(AlarmSeverity.WARNING); doPost("/api/alarm", alarmMid, Alarm.class); - Thread.sleep(500); - // Filter 1: temperature > 50 KeyFilter tempGt50 = new KeyFilter(); tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); @@ -2055,6 +2048,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // OR query: should return alarms for deviceHot (60>50) and deviceCold (5<10) = 2 alarms AlarmDataQuery orAlarmQuery = new AlarmDataQuery(entityFilter, pageLink, null, null, keyFilters, alarmFields, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findAlarmsByQueryAndCheck(orAlarmQuery, 2)); PageData alarmResult = findAlarmsByQueryAndCheck(orAlarmQuery, 2); List alarmTypes = alarmResult.getData().stream().map(AlarmData::getType).collect(Collectors.toList()); assertThat(alarmTypes).containsExactlyInAnyOrder("highTemp", "lowTemp"); @@ -2089,8 +2083,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { doPost("/api/plugins/telemetry/" + deviceMid.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"temperature\":30}", String.class, status().isOk()); - Thread.sleep(500); - // Create 2 alarms for deviceHot, 1 for deviceCold, 1 for deviceMid Alarm alarm1 = new Alarm(); alarm1.setOriginator(deviceHot.getId()); @@ -2116,8 +2108,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { alarm4.setSeverity(AlarmSeverity.WARNING); doPost("/api/alarm", alarm4, Alarm.class); - Thread.sleep(500); - KeyFilter tempGt50 = new KeyFilter(); tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); tempGt50.setValueType(EntityKeyValueType.NUMERIC); @@ -2142,7 +2132,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // OR: deviceHot (2 alarms) + deviceCold (1 alarm) match => 3 alarms total AlarmCountQuery orQuery = new AlarmCountQuery(entityFilter, keyFilters, ComplexOperation.OR); - countAlarmsByQueryAndCheck(orQuery, 3); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countAlarmsByQueryAndCheck(orQuery, 3)); // AND: no device matches both filters => 0 AlarmCountQuery andQuery = new AlarmCountQuery(entityFilter, keyFilters, ComplexOperation.AND); @@ -2173,8 +2163,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"temperature\":10}", String.class, status().isOk()); - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("orMixedType")); filter.setDeviceNameFilter(""); @@ -2201,7 +2189,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // OR: deviceA matches name contains "Alpha", deviceB matches temp>50 => count=2 EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); - countByQueryAndCheck(orQuery, 2); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); // AND: only deviceA has name "Alpha" AND temp is 10 (not >50) => count=0 EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); @@ -2231,8 +2219,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"color\":\"green\"}", String.class, status().isOk()); - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("orStrType")); filter.setDeviceNameFilter(""); @@ -2259,7 +2245,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // OR: deviceA (red) and deviceB (blue) match => count=2 EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); - countByQueryAndCheck(orQuery, 2); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); // AND: no device is both red AND blue => count=0 EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); @@ -2282,8 +2268,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"temperature\":30}", String.class, status().isOk()); - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("orSingleType")); filter.setDeviceNameFilter(""); @@ -2300,6 +2284,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // Single filter with OR should behave identically to AND EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 1)); Long orResult = countByQueryAndCheck(orQuery, 1); EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); @@ -2338,8 +2323,6 @@ public class EntityQueryControllerTest extends AbstractControllerTest { doPost("/api/plugins/telemetry/" + deviceD.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"temperature\":20,\"humidity\":50,\"pressure\":1000}", String.class, status().isOk()); - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("or3fType")); filter.setDeviceNameFilter(""); @@ -2375,7 +2358,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // OR: A matches temp>50, B matches hum>80, C matches press>1040, D matches none => 3 EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); - countByQueryAndCheck(orQuery, 3); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 3)); // AND: no device matches all three => 0 EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); @@ -2385,19 +2368,17 @@ public class EntityQueryControllerTest extends AbstractControllerTest { @Test public void testFindEntityDataWithOrPagination() throws Exception { // Create 5 devices, 4 match OR filters (1,2: temp>50; 3,4: temp<10; 5: no match) + // Device 1: temp=61, Device 2: temp=62, Device 3: temp=2, Device 4: temp=1, Device 5: temp=25 for (int i = 1; i <= 5; i++) { Device device = new Device(); device.setName(String.format("OrPageDevice%02d", i)); device.setType("orPageType"); device = doPost("/api/device", device, Device.class); - // Devices 1,2: temperature > 50. Device 3,4: temperature < 10. Device 5: no match (25). int temp = (i <= 2) ? 60 + i : (i <= 4) ? 5 - i : 25; doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"temperature\":" + temp + "}", String.class, status().isOk()); } - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("orPageType")); filter.setDeviceNameFilter(""); @@ -2428,6 +2409,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // Page 1: pageSize=2, totalElements=4, data.size()=2 EntityDataPageLink pageLink1 = new EntityDataPageLink(2, 0, null, sortOrder); EntityDataQuery orQuery1 = new EntityDataQuery(filter, pageLink1, entityFields, null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQuery1, 4)); PageData page1 = findByQueryAndCheck(orQuery1, 4); Assert.assertEquals(2, page1.getData().size()); Assert.assertTrue(page1.hasNext()); @@ -2463,12 +2445,21 @@ public class EntityQueryControllerTest extends AbstractControllerTest { doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, "{\"temperature\":40}", String.class, status().isOk()); - Thread.sleep(1000); - DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("orZeroType")); filter.setDeviceNameFilter(""); + // Await attribute propagation: verify devices are queryable by a filter that matches + KeyFilter tempGt20 = new KeyFilter(); + tempGt20.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt20.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt20 = new NumericFilterPredicate(); + gt20.setValue(FilterPredicateValue.fromDouble(20)); + gt20.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt20.setPredicate(gt20); + EntityCountQuery propagationCheck = new EntityCountQuery(filter, List.of(tempGt20), ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(propagationCheck, 2)); + // Filter 1: temperature > 50 (no match) KeyFilter tempGt50 = new KeyFilter(); tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); @@ -2498,7 +2489,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { public void testOrKeyFiltersOperationRejectedWhenDisabled() throws Exception { loginTenantAdmin(); - baseEntityService.setKeyFiltersOrConditionsEnabled(false); + ReflectionTestUtils.setField(baseEntityService, "keyFiltersOrConditionsEnabled", false); try { DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("default")); @@ -2519,8 +2510,212 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityCountQuery explicitAndQuery = new EntityCountQuery(filter, Collections.emptyList(), ComplexOperation.AND); doPost("/api/entitiesQuery/count", explicitAndQuery).andExpect(status().isOk()); } finally { - baseEntityService.setKeyFiltersOrConditionsEnabled(true); + ReflectionTestUtils.setField(baseEntityService, "keyFiltersOrConditionsEnabled", true); } } + @Test + public void testFindEntityDataWithOrAndTextSearch() throws Exception { + // Create 3 devices: 2 match OR filters, but only 1 also matches textSearch + Device deviceA = new Device(); + deviceA.setName("OrTextAlpha"); + deviceA.setType("orTextType"); + deviceA = doPost("/api/device", deviceA, Device.class); + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":60}", String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("OrTextBeta"); + deviceB.setType("orTextType"); + deviceB = doPost("/api/device", deviceB, Device.class); + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":5}", String.class, status().isOk()); + + Device deviceC = new Device(); + deviceC.setName("OrTextGamma"); + deviceC.setType("orTextType"); + deviceC = doPost("/api/device", deviceC, Device.class); + doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":30}", String.class, status().isOk()); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orTextType")); + filter.setDeviceNameFilter(""); + + // Filter 1: temperature > 50 (matches deviceA) + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + // Filter 2: temperature < 10 (matches deviceB) + KeyFilter tempLt10 = new KeyFilter(); + tempLt10.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempLt10.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lt10 = new NumericFilterPredicate(); + lt10.setValue(FilterPredicateValue.fromDouble(10)); + lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); + tempLt10.setPredicate(lt10); + + List keyFilters = List.of(tempGt50, tempLt10); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.ASC + ); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + // OR without textSearch: deviceA and deviceB match => 2 + EntityDataPageLink pageLinkNoText = new EntityDataPageLink(10, 0, null, sortOrder); + EntityDataQuery orQueryNoText = new EntityDataQuery(filter, pageLinkNoText, entityFields, null, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> findByQueryAndCheck(orQueryNoText, 2)); + + // OR with textSearch="Alpha": only deviceA matches both OR filter AND text search + EntityDataPageLink pageLinkWithText = new EntityDataPageLink(10, 0, "Alpha", sortOrder); + EntityDataQuery orQueryWithText = new EntityDataQuery(filter, pageLinkWithText, entityFields, null, keyFilters, ComplexOperation.OR); + PageData result = findByQueryAndCheck(orQueryWithText, 1); + String name = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + assertThat(name).isEqualTo("OrTextAlpha"); + + // OR with textSearch="Beta": only deviceB matches both OR filter AND text search + EntityDataPageLink pageLinkBeta = new EntityDataPageLink(10, 0, "Beta", sortOrder); + EntityDataQuery orQueryBeta = new EntityDataQuery(filter, pageLinkBeta, entityFields, null, keyFilters, ComplexOperation.OR); + PageData resultBeta = findByQueryAndCheck(orQueryBeta, 1); + String nameBeta = resultBeta.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + assertThat(nameBeta).isEqualTo("OrTextBeta"); + + // OR with textSearch="Gamma": deviceC doesn't match any OR filter => 0 + EntityDataPageLink pageLinkGamma = new EntityDataPageLink(10, 0, "Gamma", sortOrder); + EntityDataQuery orQueryGamma = new EntityDataQuery(filter, pageLinkGamma, entityFields, null, keyFilters, ComplexOperation.OR); + findByQueryAndCheck(orQueryGamma, 0); + } + + @Test + public void testCountEntitiesWithOrTimeSeriesKeyFilters() throws Exception { + // Create devices and post time-series telemetry (not attributes) + Device deviceA = new Device(); + deviceA.setName("OrTsDeviceA"); + deviceA.setType("orTsType"); + deviceA = doPost("/api/device", deviceA, Device.class); + JsonNode tsPayloadA = JacksonUtil.toJsonNode("{\"temperature\": 60}"); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + deviceA.getUuidId() + "/timeseries/SERVER_SCOPE", tsPayloadA) + .andExpect(status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("OrTsDeviceB"); + deviceB.setType("orTsType"); + deviceB = doPost("/api/device", deviceB, Device.class); + JsonNode tsPayloadB = JacksonUtil.toJsonNode("{\"temperature\": 5}"); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + deviceB.getUuidId() + "/timeseries/SERVER_SCOPE", tsPayloadB) + .andExpect(status().isOk()); + + Device deviceC = new Device(); + deviceC.setName("OrTsDeviceC"); + deviceC.setType("orTsType"); + deviceC = doPost("/api/device", deviceC, Device.class); + JsonNode tsPayloadC = JacksonUtil.toJsonNode("{\"temperature\": 30}"); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + deviceC.getUuidId() + "/timeseries/SERVER_SCOPE", tsPayloadC) + .andExpect(status().isOk()); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orTsType")); + filter.setDeviceNameFilter(""); + + // Filter 1: TIME_SERIES temperature > 50 + KeyFilter tempGt50 = new KeyFilter(); + tempGt50.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + tempGt50.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + tempGt50.setPredicate(gt50); + + // Filter 2: TIME_SERIES temperature < 10 + KeyFilter tempLt10 = new KeyFilter(); + tempLt10.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + tempLt10.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lt10 = new NumericFilterPredicate(); + lt10.setValue(FilterPredicateValue.fromDouble(10)); + lt10.setOperation(NumericFilterPredicate.NumericOperation.LESS); + tempLt10.setPredicate(lt10); + + List keyFilters = List.of(tempGt50, tempLt10); + + // OR: deviceA (60>50) and deviceB (5<10) match => count=2 + EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); + + // AND: no device has ts temperature both >50 AND <10 => count=0 + EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + + @Test + public void testCountEntitiesWithOrComplexFilterPredicate() throws Exception { + // Tests key-level ComplexFilterPredicate combined with query-level OR + Device deviceA = new Device(); + deviceA.setName("OrCplxDeviceA"); + deviceA.setType("orCplxType"); + deviceA = doPost("/api/device", deviceA, Device.class); + doPost("/api/plugins/telemetry/" + deviceA.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":65,\"humidity\":50}", String.class, status().isOk()); + + Device deviceB = new Device(); + deviceB.setName("OrCplxDeviceB"); + deviceB.setType("orCplxType"); + deviceB = doPost("/api/device", deviceB, Device.class); + doPost("/api/plugins/telemetry/" + deviceB.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":25,\"humidity\":90}", String.class, status().isOk()); + + Device deviceC = new Device(); + deviceC.setName("OrCplxDeviceC"); + deviceC.setType("orCplxType"); + deviceC = doPost("/api/device", deviceC, Device.class); + doPost("/api/plugins/telemetry/" + deviceC.getId() + "/" + DataConstants.SHARED_SCOPE, + "{\"temperature\":25,\"humidity\":50}", String.class, status().isOk()); + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("orCplxType")); + filter.setDeviceNameFilter(""); + + // Key filter 1: temperature > 50 AND temperature < 70 (complex predicate within key filter) + // Matches deviceA (65) only + NumericFilterPredicate gt50 = new NumericFilterPredicate(); + gt50.setValue(FilterPredicateValue.fromDouble(50)); + gt50.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + NumericFilterPredicate lt70 = new NumericFilterPredicate(); + lt70.setValue(FilterPredicateValue.fromDouble(70)); + lt70.setOperation(NumericFilterPredicate.NumericOperation.LESS); + ComplexFilterPredicate complexTempPred = new ComplexFilterPredicate(); + complexTempPred.setOperation(ComplexOperation.AND); + complexTempPred.setPredicates(List.of(gt50, lt70)); + + KeyFilter tempComplexFilter = new KeyFilter(); + tempComplexFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + tempComplexFilter.setValueType(EntityKeyValueType.NUMERIC); + tempComplexFilter.setPredicate(complexTempPred); + + // Key filter 2: humidity > 80 (simple predicate) + // Matches deviceB (90) only + KeyFilter humFilter = new KeyFilter(); + humFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidity")); + humFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate humGt80 = new NumericFilterPredicate(); + humGt80.setValue(FilterPredicateValue.fromDouble(80)); + humGt80.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + humFilter.setPredicate(humGt80); + + List keyFilters = List.of(tempComplexFilter, humFilter); + + // Query-level OR: deviceA matches key filter 1, deviceB matches key filter 2 => 2 + EntityCountQuery orQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.OR); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> countByQueryAndCheck(orQuery, 2)); + + // Query-level AND: no device matches both key filters => 0 + EntityCountQuery andQuery = new EntityCountQuery(filter, keyFilters, ComplexOperation.AND); + countByQueryAndCheck(andQuery, 0); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 98a1113157..7367d43910 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -19,7 +19,6 @@ import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -94,7 +93,6 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe private static final Set EXCLUDED_TYPES_FROM_OPTIMIZATION = Set.of( EntityFilterType.ENTITY_LIST, EntityFilterType.SINGLE_ENTITY, EntityFilterType.RELATIONS_QUERY); - @Setter @Value("${sql.query.key-filters-or-conditions.enabled:true}") private boolean keyFiltersOrConditionsEnabled; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 28fa6bec0f..100c733df6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -297,12 +297,15 @@ public class EntityKeyMapping { // "cast(e.name as varchar) as alias2"). Temporarily null out entityKeyColumn // so buildSimplePredicateQuery uses the alias directly as the field. String savedColumn = this.entityKeyColumn; - this.entityKeyColumn = null; - List predicates = keyFilters.stream() - .map(keyFilter -> this.buildKeyQuery(ctx, alias, keyFilter, filterType)) - .collect(Collectors.toList()); - this.entityKeyColumn = savedColumn; - return predicates.stream(); + try { + this.entityKeyColumn = null; + List predicates = keyFilters.stream() + .map(keyFilter -> this.buildKeyQuery(ctx, alias, keyFilter, filterType)) + .collect(Collectors.toList()); + return predicates.stream(); + } finally { + this.entityKeyColumn = savedColumn; + } } else { keyAlias = "e"; } diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java index a7846c0674..ab1b6c4060 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RepositoryUtilsTest.java @@ -585,4 +585,49 @@ public class RepositoryUtilsTest { assertThat(RepositoryUtils.checkKeyFilters(deviceData, Collections.emptyList(), ComplexOperation.OR)).isTrue(); } + @Test + public void testCheckKeyFiltersWithOrStringEntityFieldFilters() { + // Entity with name="loranet device 123" + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name("loranet device 123").build()); + + // Filter 1: name STARTS_WITH "mqtt" (no match) + EdqsFilter nameStartsMqtt = getStringEntityFieldFilter("name", StringOperation.STARTS_WITH, "mqtt"); + // Filter 2: name CONTAINS "lora" (match) + EdqsFilter nameContainsLora = getStringEntityFieldFilter("name", StringOperation.CONTAINS, "lora"); + + // OR: second filter matches + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(nameStartsMqtt, nameContainsLora), ComplexOperation.OR)).isTrue(); + + // AND: first filter fails + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(nameStartsMqtt, nameContainsLora), ComplexOperation.AND)).isFalse(); + } + + @Test + public void testCheckKeyFiltersWithOrEntityFieldNullStringSpecialCase() { + // Entity with name=null — tests the special case where null string + entity field = match + DeviceData deviceData = new DeviceData(UUID.randomUUID()); + deviceData.setCustomerId(UUID.randomUUID()); + deviceData.setFields(DeviceFields.builder().name(null).build()); + + // Filter on entity field "label" which is not set (null) + EdqsFilter labelFilter = getStringEntityFieldFilter("label", StringOperation.STARTS_WITH, "test"); + // Filter on timeseries (non-entity-field) that doesn't exist + EdqsFilter tempFilter = getTemperatureFilter(NumericOperation.GREATER, 50); + + // Entity field with null string = match (special case in evaluateSingleFilter) + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(labelFilter), ComplexOperation.OR)).isTrue(); + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(labelFilter), ComplexOperation.AND)).isTrue(); + + // Non-entity-field with missing data = no match + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(tempFilter), ComplexOperation.OR)).isFalse(); + + // OR: entity field null match + missing timeseries = true (first matches) + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(labelFilter, tempFilter), ComplexOperation.OR)).isTrue(); + + // AND: entity field null match + missing timeseries = false (second fails) + assertThat(RepositoryUtils.checkKeyFilters(deviceData, List.of(labelFilter, tempFilter), ComplexOperation.AND)).isFalse(); + } + } From 7f8d2552cf143487cab209acfa6a6667ae0953da Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 25 Mar 2026 00:27:40 +0100 Subject: [PATCH 200/313] Additional fixes --- .../rule-node/action/create-relation-config.component.ts | 4 ---- .../rule-node/action/delete-relation-config.component.ts | 4 ---- .../transformation/deduplication-config.component.ts | 4 ---- .../pages/notification/sent/sent-error-dialog.component.html | 2 +- .../pages/notification/sent/sent-error-dialog.component.ts | 3 ++- 5 files changed, 3 insertions(+), 14 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts index 89e2e3be5d..1939b4abbf 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.ts @@ -105,8 +105,4 @@ export class CreateRelationConfigComponent extends RuleNodeConfigurationComponen configuration.entityTypePattern = configuration.entityTypePattern ? configuration.entityTypePattern.trim() : null; return configuration; } - - getDirectionTypeTranslations(type: string) { - return this.directionTypeTranslations.get(type as EntitySearchDirection); - } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts index b32dc1f157..d41def61d3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.ts @@ -99,8 +99,4 @@ export class DeleteRelationConfigComponent extends RuleNodeConfigurationComponen configuration.entityNamePattern = configuration.entityNamePattern ? configuration.entityNamePattern.trim() : null; return configuration; } - - getDirectionTypeTranslations(type: string) { - return this.directionTypeTranslations.get(type as EntitySearchDirection); - } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts index f37b71299f..1efcc0351a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.ts @@ -77,8 +77,4 @@ export class DeduplicationConfigComponent extends RuleNodeConfigurationComponent protected validatorTriggers(): string[] { return ['strategy']; } - - getDeduplicationStrategiesTranslations(strategy: string) { - return this.deduplicationStrategiesTranslations.get(strategy as FetchMode); - } } diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html index 53f554e706..1c98892b60 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.html @@ -29,7 +29,7 @@
{{ NotificationDeliveryMethodInfoMap.get(errorStat.key).name | translate }}
- @for (error of errorStatEntries; track error) { + @for (error of (errorStat.value | keyvalue); track error) {
{{ error.key }} diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts index a81bce6eae..1dfc3b44ca 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.ts @@ -50,7 +50,8 @@ export class SentErrorDialogComponent extends DialogComponent).map(([key, value]) => ({ key, value })); } + this.errorStatEntries = (Object.entries(this.errorStats) as Array<[NotificationDeliveryMethod, { [k: string]: string }]>).map(([key, value]) => ({ key, value })); + } cancel(): void { this.dialogRef.close(null); From f7b79f8dc677c1adf71be767f5e3ae4ee84fd30b Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 25 Mar 2026 09:56:22 +0200 Subject: [PATCH 201/313] Cleanup --- .../server/service/query/DefaultEntityQueryService.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 7758fe56e5..92d76693f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -18,7 +18,6 @@ package org.thingsboard.server.service.query; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; - import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -105,13 +104,11 @@ public class DefaultEntityQueryService implements EntityQueryService { @Override public long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query) { - return entityService.countEntitiesByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); } @Override public PageData findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) { - if (query.getKeyFilters() != null) { resolveDynamicValuesInPredicates( query.getKeyFilters().stream() @@ -181,7 +178,6 @@ public class DefaultEntityQueryService implements EntityQueryService { @Override public PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) { - EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); PageData entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), entityDataQuery); @@ -208,7 +204,6 @@ public class DefaultEntityQueryService implements EntityQueryService { @Override public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) { - if (query.getEntityFilter() != null) { EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); PageData entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), @@ -241,8 +236,6 @@ public class DefaultEntityQueryService implements EntityQueryService { return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters(), query.getKeyFiltersOperationOrDefault()); } - - @Override public ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, boolean isTimeseries, boolean isAttributes, AttributeScope scope) { From 729ac2c3f8eb96ba881cdf239d78d44f19561f59 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 25 Mar 2026 16:18:41 +0200 Subject: [PATCH 202/313] Fixes after review --- .../server/common/data/rule/RuleChainDetails.java | 3 ++- .../java/org/thingsboard/common/util/JacksonUtil.java | 8 ++++++++ .../server/dao/model/sql/RuleChainDetailsEntity.java | 6 ++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java index c09cc8aef3..439fcc01d1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.ArrayList; import java.util.List; @Data @@ -40,7 +41,7 @@ public class RuleChainDetails extends RuleChain { public RuleChainDetails(RuleChainDetails ruleChainDetails) { super(ruleChainDetails); - this.notes = ruleChainDetails.getNotes(); + this.notes = ruleChainDetails.getNotes() != null ? new ArrayList<>(ruleChainDetails.getNotes()) : null; } } diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 2cf758885f..cf547cb9d4 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -243,6 +243,14 @@ public class JacksonUtil { } } + public static T treeToValue(JsonNode node, TypeReference type) { + try { + return OBJECT_MAPPER.treeToValue(node, type); + } catch (IOException e) { + throw new IllegalArgumentException("Can't convert value: " + node.toString(), e); + } + } + public static JsonNode toJsonNode(String value) { return toJsonNode(value, OBJECT_MAPPER); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainDetailsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainDetailsEntity.java index 732687016e..bb7680e476 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainDetailsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainDetailsEntity.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.model.sql; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.Column; import jakarta.persistence.Convert; @@ -25,12 +26,9 @@ import lombok.EqualsAndHashCode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainDetails; -import org.thingsboard.server.common.data.rule.RuleChainNote; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.util.mapping.JsonConverter; -import java.util.Arrays; - @Data @EqualsAndHashCode(callSuper = true) @Entity @@ -57,7 +55,7 @@ public class RuleChainDetailsEntity extends AbstractRuleChainEntity() {})); } return details; } From 27ee393028c8e114d7cf21cb28d49018f481949e Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 26 Mar 2026 12:33:17 +0200 Subject: [PATCH 203/313] CE: Added request body template support for REST API call node --- .../rule/engine/api/util/TbNodeUtils.java | 32 ++- .../rule/engine/api/util/TbNodeUtilsTest.java | 44 ++++ .../rule/engine/rest/TbHttpClient.java | 21 +- .../rule/engine/rest/TbRestApiCallNode.java | 4 +- .../rest/TbRestApiCallNodeConfiguration.java | 1 + .../rule/engine/rest/TbHttpClientTest.java | 4 +- .../engine/rest/TbRestApiCallNodeTest.java | 217 ++++++++++++++---- .../rest/TbSendRestApiCallReplyNodeTest.java | 3 +- .../rest-api-call-config.component.html | 13 ++ .../rest-api-call-config.component.ts | 1 + .../assets/locale/locale.constant-en_US.json | 5 +- 11 files changed, 280 insertions(+), 65 deletions(-) diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index 4a0f1883c0..8fac42e029 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.api.util; +import com.fasterxml.jackson.core.io.JsonStringEncoder; import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -57,8 +58,12 @@ public final class TbNodeUtils { } public static String processPattern(String pattern, TbMsg tbMsg) { + return processPattern(pattern, tbMsg, false); + } + + public static String processPattern(String pattern, TbMsg tbMsg, boolean escapeJsonValues) { try { - String result = processPattern(pattern, tbMsg.getMetaData()); + String result = processPattern(pattern, tbMsg.getMetaData(), escapeJsonValues); JsonNode json = JacksonUtil.toJsonNode(tbMsg.getData()); result = result.replace(ALL_DATA_TEMPLATE, JacksonUtil.toString(json)); @@ -79,7 +84,11 @@ public final class TbNodeUtils { } if (jsonNode != null && jsonNode.isValueNode()) { - result = result.replace(formatDataVarTemplate(group), jsonNode.asText()); + String value = jsonNode.asText(); + if (escapeJsonValues) { + value = escapeJsonValue(value); + } + result = result.replace(formatDataVarTemplate(group), value); } } } @@ -89,22 +98,31 @@ public final class TbNodeUtils { } } - private static String processPattern(String pattern, TbMsgMetaData metaData) { + private static String processPattern(String pattern, TbMsgMetaData metaData, boolean escapeJsonValues) { String replacement = metaData.isEmpty() ? "{}" : JacksonUtil.toString(metaData.getData()); pattern = pattern.replace(ALL_METADATA_TEMPLATE, replacement); - return processTemplate(pattern, metaData.values()); + return processTemplate(pattern, metaData.values(), escapeJsonValues); + } + + private static String processPattern(String pattern, TbMsgMetaData metaData) { + return processPattern(pattern, metaData, false); } public static String processTemplate(String template, Map data) { + return processTemplate(template, data, false); + } + + private static String processTemplate(String template, Map data, boolean escapeJsonValues) { String result = template; for (Map.Entry kv : data.entrySet()) { - result = processVar(result, kv.getKey(), kv.getValue()); + String value = escapeJsonValues ? escapeJsonValue(kv.getValue()) : kv.getValue(); + result = result.replace(formatMetadataVarTemplate(kv.getKey()), value); } return result; } - private static String processVar(String pattern, String key, String val) { - return pattern.replace(formatMetadataVarTemplate(key), val); + private static String escapeJsonValue(String value) { + return new String(JsonStringEncoder.getInstance().quoteAsString(value)); } static String formatDataVarTemplate(String key) { diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java index 18003d6b70..882e2080d7 100644 --- a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java @@ -305,6 +305,50 @@ public class TbNodeUtilsTest { assertThat(actual, is(expected)); } + @Test + public void testProcessPatternWithJsonEscaping() { + String pattern = "{\"name\":\"${user}\",\"desc\":\"$[description]\"}"; + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("user", "John \"Doe\""); + + ObjectNode node = JacksonUtil.newObjectNode(); + node.put("description", "line1\nline2"); + + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TenantId.SYS_TENANT_ID) + .copyMetaData(md) + .data(JacksonUtil.toString(node)) + .build(); + + String result = TbNodeUtils.processPattern(pattern, msg, true); + Assertions.assertEquals("{\"name\":\"John \\\"Doe\\\"\",\"desc\":\"line1\\nline2\"}", result); + + // Verify the result is valid JSON + Assertions.assertDoesNotThrow(() -> JacksonUtil.toJsonNode(result)); + } + + @Test + public void testProcessPatternWithoutJsonEscaping() { + String pattern = "Hello ${user}, desc: $[description]"; + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("user", "John \"Doe\""); + + ObjectNode node = JacksonUtil.newObjectNode(); + node.put("description", "line1\nline2"); + + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TenantId.SYS_TENANT_ID) + .copyMetaData(md) + .data(JacksonUtil.toString(node)) + .build(); + + // Without escaping, raw values are substituted as-is + String result = TbNodeUtils.processPattern(pattern, msg, false); + Assertions.assertEquals("Hello John \"Doe\", desc: line1\nline2", result); + } + @Test public void testMixedAllDataMetadataAndNormalTemplates() { // GIVEN diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 8db6a3ccaf..4a855f1579 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -55,6 +55,7 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -182,10 +183,7 @@ public class TbHttpClient { } EventLoopGroup getSharedOrCreateEventLoopGroup(EventLoopGroup eventLoopGroupShared) { - if (eventLoopGroupShared != null) { - return eventLoopGroupShared; - } - return this.eventLoopGroup = new NioEventLoopGroup(); + return Objects.requireNonNullElseGet(eventLoopGroupShared, () -> this.eventLoopGroup = new NioEventLoopGroup()); } private void checkSystemProxyProperties() throws TbNodeException { @@ -243,7 +241,7 @@ public class TbHttpClient { if ((HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) || HttpMethod.PATCH.equals(method) || HttpMethod.DELETE.equals(method)) && !config.isIgnoreRequestBody()) { - request.body(BodyInserters.fromValue(getData(msg, config.isParseToPlainText()))); + request.body(BodyInserters.fromValue(getRequestBody(msg))); } request @@ -275,7 +273,7 @@ public class TbHttpClient { if (origin instanceof WebClientResponseException restClientResponseException && restClientResponseException.getStatusCode().is2xxSuccessful()) { // return cause instead of original exception in case 2xx status code - // this will provide meaningful error message to the user + // this will provide a meaningful error message to the user return new RuntimeException(restClientResponseException.getCause()); } return origin; @@ -340,6 +338,15 @@ public class TbHttpClient { return UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri(); } + private Object getRequestBody(TbMsg msg) { + if (StringUtils.isNotBlank(config.getRequestBodyTemplate())) { + boolean escapeJson = !config.isParseToPlainText(); + String processedTemplate = TbNodeUtils.processPattern(config.getRequestBodyTemplate(), msg, escapeJson); + return config.isParseToPlainText() ? processedTemplate : JacksonUtil.toJsonNode(processedTemplate); + } + return getData(msg, config.isParseToPlainText()); + } + private Object getData(TbMsg tbMsg, boolean parseToPlainText) { String data = tbMsg.getData(); return parseToPlainText ? JacksonUtil.toPlainText(data) : JacksonUtil.toJsonNode(data); @@ -363,7 +370,7 @@ public class TbHttpClient { headers.forEach((key, values) -> { if (values != null && !values.isEmpty()) { if (values.size() == 1) { - consumer.accept(key, values.get(0)); + consumer.accept(key, values.getFirst()); } else { consumer.accept(key, JacksonUtil.toString(values)); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index 5ffe37b102..50f4ef8d9d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -41,7 +41,9 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie version = 4, nodeDescription = "Invoke REST API calls to external REST server", nodeDetails = "Will invoke REST API call GET | POST | PUT | DELETE to external REST server. " + - "Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." + + "Message payload is used as the request body by default. " + + "Optionally, a request body template can be configured with ${metadataKey} and $[messageKey] placeholders. " + + "Configured attributes can be added into Headers from Message Metadata." + " Outbound message will contain response fields " + "(status, statusCode, statusReason and response headers) in the Message Metadata." + " Response body saved in outbound Message payload. " + diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index 9377d83949..0994ec19e3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -51,6 +51,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration data = metaData.getData(); Assertions.assertEquals(2, data.size()); - Assertions.assertEquals(data.get("Content-Type"), "binary"); - Assertions.assertEquals(data.get("Set-Cookie"), "[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]"); + Assertions.assertEquals("binary", data.get("Content-Type")); + Assertions.assertEquals("[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]", data.get("Set-Cookie")); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index 3d3abde30c..fbe4aa4136 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -16,13 +16,9 @@ package org.thingsboard.rule.engine.rest; import com.datastax.oss.driver.api.core.uuid.Uuids; -import org.apache.http.HttpException; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; import org.apache.http.config.SocketConfig; import org.apache.http.impl.bootstrap.HttpServer; import org.apache.http.impl.bootstrap.ServerBootstrap; -import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestHandler; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -52,6 +48,8 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.exception.DataValidationException; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -79,11 +77,11 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { @Mock private TbContext ctx; - private EntityId originator = new DeviceId(Uuids.timeBased()); - private TbMsgMetaData metaData = new TbMsgMetaData(); + private final EntityId originator = new DeviceId(Uuids.timeBased()); + private final TbMsgMetaData metaData = new TbMsgMetaData(); - private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + private final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); private HttpServer server; @@ -217,22 +215,17 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { public void deleteRequestWithoutBody() throws IOException, InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final String path = "/path/to/delete"; - setupServer("*", new HttpRequestHandler() { - - @Override - public void handle(HttpRequest request, HttpResponse response, HttpContext context) - throws HttpException, IOException { - try { - assertEquals(request.getRequestLine().getUri(), path, "Request path matches"); - assertTrue(request.containsHeader("Foo"), "Custom header included"); - assertEquals("Bar", request.getFirstHeader("Foo").getValue(), "Custom header value"); - response.setStatusCode(200); - latch.countDown(); - } catch (Exception e) { - System.out.println("Exception handling request: " + e.toString()); - e.printStackTrace(); - latch.countDown(); - } + setupServer("*", (request, response, _) -> { + try { + assertEquals(path, request.getRequestLine().getUri(), "Request path matches"); + assertTrue(request.containsHeader("Foo"), "Custom header included"); + assertEquals("Bar", request.getFirstHeader("Foo").getValue(), "Custom header value"); + response.setStatusCode(200); + latch.countDown(); + } catch (Exception e) { + System.out.println("Exception handling request: " + e); + e.printStackTrace(); + latch.countDown(); } }); @@ -269,28 +262,23 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { public void deleteRequestWithBody() throws IOException, InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final String path = "/path/to/delete"; - setupServer("*", new HttpRequestHandler() { - - @Override - public void handle(HttpRequest request, HttpResponse response, HttpContext context) - throws HttpException, IOException { - try { - assertEquals(path, request.getRequestLine().getUri(), "Request path matches"); - assertTrue(request.containsHeader("Content-Type"), "Content-Type included"); - assertEquals("application/json", - request.getFirstHeader("Content-Type").getValue(), "Content-Type value"); - assertTrue(request.containsHeader("Content-Length"), "Content-Length included"); - assertEquals("2", - request.getFirstHeader("Content-Length").getValue(), "Content-Length value"); - assertTrue(request.containsHeader("Foo"), "Custom header included"); - assertEquals("Bar", request.getFirstHeader("Foo").getValue(), "Custom header value"); - response.setStatusCode(200); - latch.countDown(); - } catch (Exception e) { - System.out.println("Exception handling request: " + e.toString()); - e.printStackTrace(); - latch.countDown(); - } + setupServer("*", (request, response, _) -> { + try { + assertEquals(path, request.getRequestLine().getUri(), "Request path matches"); + assertTrue(request.containsHeader("Content-Type"), "Content-Type included"); + assertEquals("application/json", + request.getFirstHeader("Content-Type").getValue(), "Content-Type value"); + assertTrue(request.containsHeader("Content-Length"), "Content-Length included"); + assertEquals("2", + request.getFirstHeader("Content-Length").getValue(), "Content-Length value"); + assertTrue(request.containsHeader("Foo"), "Custom header included"); + assertEquals("Bar", request.getFirstHeader("Foo").getValue(), "Custom header value"); + response.setStatusCode(200); + latch.countDown(); + } catch (Exception e) { + System.out.println("Exception handling request: " + e); + e.printStackTrace(); + latch.countDown(); } }); @@ -323,6 +311,143 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue()); } + @Test + public void postRequestWithBodyTemplate() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/api/token"; + final String[] capturedBody = new String[1]; + setupServerWithBodyCapture(capturedBody, latch); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("POST"); + config.setRequestBodyTemplate("{\"grant_type\":\"client_credentials\",\"client_id\":\"${clientId}\",\"value\":\"$[token]\"}"); + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + metaData.putValue("clientId", "my-client-123"); + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .copyMetaData(metaData) + .dataType(TbMsgDataType.JSON) + .data("{\"token\":\"abc-xyz\"}") + .ruleChainId(ruleChainId) + .ruleNodeId(ruleNodeId) + .build(); + restNode.onMsg(ctx, msg); + + assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); + assertEquals("{\"grant_type\":\"client_credentials\",\"client_id\":\"my-client-123\",\"value\":\"abc-xyz\"}", capturedBody[0]); + } + + @Test + public void postRequestWithBodyTemplateAndParseToPlainText() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/api/text"; + final String[] capturedBody = new String[1]; + setupServerWithBodyCapture(capturedBody, latch); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("POST"); + config.setParseToPlainText(true); + config.setRequestBodyTemplate("Hello ${name}, your token is $[token]!"); + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + metaData.putValue("name", "World"); + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .copyMetaData(metaData) + .dataType(TbMsgDataType.JSON) + .data("{\"token\":\"abc-xyz\"}") + .ruleChainId(ruleChainId) + .ruleNodeId(ruleNodeId) + .build(); + restNode.onMsg(ctx, msg); + + assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); + assertEquals("Hello World, your token is abc-xyz!", capturedBody[0]); + } + + @Test + public void postRequestWithBodyTemplateEscapesJsonSpecialChars() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/api/token"; + final String[] capturedBody = new String[1]; + setupServerWithBodyCapture(capturedBody, latch); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("POST"); + config.setRequestBodyTemplate("{\"name\":\"${userName}\",\"desc\":\"$[description]\"}"); + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + metaData.putValue("userName", "John \"Doe\""); + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .copyMetaData(metaData) + .dataType(TbMsgDataType.JSON) + .data("{\"description\":\"line1\\nline2\"}") + .ruleChainId(ruleChainId) + .ruleNodeId(ruleNodeId) + .build(); + restNode.onMsg(ctx, msg); + + assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); + assertEquals("{\"name\":\"John \\\"Doe\\\"\",\"desc\":\"line1\\nline2\"}", capturedBody[0]); + } + + @Test + public void postRequestWithEmptyBodyTemplateUsesMessageData() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/api/data"; + final String[] capturedBody = new String[1]; + setupServerWithBodyCapture(capturedBody, latch); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("POST"); + // requestBodyTemplate is null by default — should use msg.getData() + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .copyMetaData(metaData) + .dataType(TbMsgDataType.JSON) + .data("{\"temperature\":25}") + .ruleChainId(ruleChainId) + .ruleNodeId(ruleNodeId) + .build(); + restNode.onMsg(ctx, msg); + + assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctx, timeout(10_000)).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + assertEquals("{\"temperature\":25}", capturedBody[0]); + } + + private void setupServerWithBodyCapture(String[] capturedBody, CountDownLatch latch) throws IOException { + setupServer("*", (request, response, _) -> { + try { + if (request instanceof org.apache.http.HttpEntityEnclosingRequest entityRequest) { + InputStream is = entityRequest.getEntity().getContent(); + capturedBody[0] = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + response.setStatusCode(200); + latch.countDown(); + } catch (Exception e) { + e.printStackTrace(); + latch.countDown(); + } + }); + } + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { return Stream.of( Arguments.of(0, diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java index 7b00d249ff..062e2d0a2e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java @@ -51,7 +51,7 @@ public class TbSendRestApiCallReplyNodeTest { private TbSendRestApiCallReplyNode node; private TbSendRestApiCallReplyNodeConfiguration config; - + @Mock private TbContext ctxMock; @Mock @@ -140,4 +140,5 @@ public class TbSendRestApiCallReplyNodeTest { TbMsg.EMPTY_STRING, "Request body is empty!") ); } + } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html index b2fdb3a14c..948089721c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html @@ -129,6 +129,19 @@ {{ 'rule-node-config.ignore-request-body' | translate }} +
+ +
+ + rule-node-config.request-body-template + + rule-node-config.request-body-template-empty-hint + +
rule-node-config.read-timeout diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts index c9694ccd94..32ea3d57d9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.ts @@ -51,6 +51,7 @@ export class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { requestMethod: [configuration ? configuration.requestMethod : null, [Validators.required]], parseToPlainText: [configuration ? configuration.parseToPlainText : false, []], ignoreRequestBody: [configuration ? configuration.ignoreRequestBody : false, []], + requestBodyTemplate: [configuration ? configuration.requestBodyTemplate : null, []], enableProxy: [configuration ? configuration.enableProxy : false, []], useSystemProxyProperties: [configuration ? configuration.enableProxy : false, []], proxyHost: [configuration ? configuration.proxyHost : null, []], diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4fdc707c1e..0dddc1a32e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5511,8 +5511,11 @@ "endpoint-url-pattern-required": "Endpoint URL pattern is required", "request-method": "Request method", "ignore-request-body": "Without request body", + "request-body-template": "Request body template", + "request-body-template-hint": "Use ${metadataKey} for value from metadata, $[messageKey] for value from message body", + "request-body-template-empty-hint": "If empty, the incoming message payload is used as the request body. Supports any content type — use 'Parse to plain text' option to send non-JSON content", "parse-to-plain-text": "Parse to plain text", - "parse-to-plain-text-hint": "If selected, request body message payload will be transformed from JSON string to plain text, e.g. msg = \"Hello,\\t\"world\"\" will be parsed to Hello, \"world\"", + "parse-to-plain-text-hint": "If selected, the request body (message payload or body template result) will be sent as a plain text string instead of being parsed as JSON, e.g. msg = \"Hello,\\t\"world\"\" will be parsed to Hello, \"world\"", "read-timeout": "Read timeout in millis", "read-timeout-hint": "The value of 0 means an infinite timeout", "max-parallel-requests-count": "Max number of parallel requests", From 6544f471011975dd7c11296c988fc28e7271f8b6 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 26 Mar 2026 13:29:10 +0200 Subject: [PATCH 204/313] no activity event on device disconnect to avoid update device state form false to true on device session timeout --- .../service/DefaultTransportService.java | 4 +- .../service/TransportActivityManagerTest.java | 58 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 260957c990..4f0c065f57 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -508,7 +508,9 @@ public class DefaultTransportService extends TransportActivityManager implements @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SessionEventMsg msg, TransportServiceCallback callback) { if (checkLimits(sessionInfo, msg, callback)) { - recordActivityInternal(sessionInfo); + if (msg.getEvent() != TransportProtos.SessionEvent.CLOSED) { + recordActivityInternal(sessionInfo); + } sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo) .setSessionEvent(msg).build(), callback); } diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java index 3b3ce6e600..a656b22241 100644 --- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java @@ -26,12 +26,14 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.activity.ActivityReportCallback; import org.thingsboard.server.common.transport.activity.ActivityState; import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategy; import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategyType; +import org.thingsboard.server.common.transport.limits.TransportRateLimitService; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.UUID; @@ -41,7 +43,12 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -514,4 +521,55 @@ public class TransportActivityManagerTest { verify(transportServiceMock, never()).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); } + @Test + void givenSessionClosedEvent_whenProcessingSessionEvent_thenShouldNotRecordActivity() { + // GIVEN - simulates the session-expiry path: session already removed from sessions map, + // then process(SESSION_CLOSED) is called. Activity must NOT be recorded to avoid active + // status update from false to true + var rateLimitServiceMock = mock(TransportRateLimitService.class); + ReflectionTestUtils.setField(transportServiceMock, "rateLimitService", rateLimitServiceMock); + when(rateLimitServiceMock.checkLimits(any(), nullable(DeviceId.class), any(), anyInt(), anyBoolean())).thenReturn(null); + + TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder() + .setSessionIdMSB(SESSION_ID.getMostSignificantBits()) + .setSessionIdLSB(SESSION_ID.getLeastSignificantBits()) + .build(); + doCallRealMethod().when(transportServiceMock).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); + + // WHEN + transportServiceMock.process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); + + // THEN + verify(transportServiceMock, never()).onActivity(any(), any(), anyLong()); + verify(transportServiceMock).sendToDeviceActor(eq(sessionInfo), any(), isNull()); + } + + @Test + void givenSessionOpenEvent_whenProcessingSessionEvent_thenShouldRecordActivity() { + // GIVEN + var rateLimitServiceMock = mock(TransportRateLimitService.class); + ReflectionTestUtils.setField(transportServiceMock, "rateLimitService", rateLimitServiceMock); + when(rateLimitServiceMock.checkLimits(any(), nullable(DeviceId.class), any(), anyInt(), anyBoolean())).thenReturn(null); + + TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder() + .setSessionIdMSB(SESSION_ID.getMostSignificantBits()) + .setSessionIdLSB(SESSION_ID.getLeastSignificantBits()) + .build(); + var sessionOpenMsg = TransportProtos.SessionEventMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC) + .setEvent(TransportProtos.SessionEvent.OPEN) + .build(); + when(transportServiceMock.toSessionId(sessionInfo)).thenReturn(SESSION_ID); + long expectedActivityTime = 100L; + when(transportServiceMock.getCurrentTimeMillis()).thenReturn(expectedActivityTime); + doCallRealMethod().when(transportServiceMock).process(sessionInfo, sessionOpenMsg, null); + + // WHEN + transportServiceMock.process(sessionInfo, sessionOpenMsg, null); + + // THEN + verify(transportServiceMock).onActivity(SESSION_ID, sessionInfo, expectedActivityTime); + verify(transportServiceMock).sendToDeviceActor(eq(sessionInfo), any(), isNull()); + } + } From 8185a3418a33d2cf1df536e9fba270348ec50265 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 26 Mar 2026 15:22:04 +0200 Subject: [PATCH 205/313] Add font installation to docker-postgres and docker-cassandra Dockerfiles Use ${docker.base.image} instead of hardcoded base image and add the SVG renderer font installation script matching tb-node Dockerfile. --- msa/tb/docker-cassandra/Dockerfile | 17 ++++++++++++++++- msa/tb/docker-postgres/Dockerfile | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 358f2907c8..59e3a2898f 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk25:trixie-slim +FROM ${docker.base.image} ENV PG_MAJOR=16 @@ -40,6 +40,21 @@ ENV PATH=$PATH:/usr/lib/postgresql/$PG_MAJOR/bin ENV PGLOG=/var/log/postgres ENV CASSANDRA_LOG=/var/log/cassandra +# For SVG renderer Debian Trixie slim fonts should be installed explicitly +RUN echo "Previously installed font libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz' || echo " (none)") \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + libharfbuzz0b fontconfig fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* \ + && echo "Current fonts libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz') \ + && echo "Font config list:" \ + && fc-list && test -n "$(fc-list)" \ + && (fc-list | grep -i dejavu) \ + && echo "Harfbuzz library test with ldconfig:" \ + && (ldconfig -p | grep harfbuzz) + COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/ RUN apt-get update \ diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index 6ce066d1ed..871154a455 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk25:trixie-slim +FROM ${docker.base.image} ENV PG_MAJOR 12 @@ -32,6 +32,21 @@ ENV SPRING_DATASOURCE_USERNAME=${pkg.user} ENV PGLOG=/var/log/postgres +# For SVG renderer Debian Trixie slim fonts should be installed explicitly +RUN echo "Previously installed font libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz' || echo " (none)") \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + libharfbuzz0b fontconfig fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* \ + && echo "Current fonts libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz') \ + && echo "Font config list:" \ + && fc-list && test -n "$(fc-list)" \ + && (fc-list | grep -i dejavu) \ + && echo "Harfbuzz library test with ldconfig:" \ + && (ldconfig -p | grep harfbuzz) + COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/ RUN apt-get update \ From 5768bbefe16ddcc69f1892ad45f87de3f7faaa75 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 31 Mar 2026 11:12:14 +0300 Subject: [PATCH 206/313] Add separate alarm rules API --- .../controller/AlarmRuleController.java | 365 +++++++ .../thingsboard/server/cf/AlarmRulesTest.java | 178 ++-- .../server/controller/AbstractWebTest.java | 5 + .../common/data/cf/AlarmRuleDefinition.java | 165 +++ .../data/cf/AlarmRuleDefinitionInfo.java | 39 + .../plans/2026-03-30-alarm-rule-controller.md | 985 ++++++++++++++++++ ...2026-03-30-alarm-rule-controller-design.md | 144 +++ .../src/app/core/http/alarm-rules.service.ts | 73 ++ .../alarm-rule-dialog.component.ts | 6 +- .../alarm-rules/alarm-rules-table-config.ts | 20 +- .../alarm-rules-table.component.ts | 6 +- .../home/pages/alarm/alarm-routing.module.ts | 6 +- 12 files changed, 1883 insertions(+), 109 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java create mode 100644 docs/superpowers/plans/2026-03-30-alarm-rule-controller.md create mode 100644 docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md create mode 100644 ui-ngx/src/app/core/http/alarm-rules.service.ts diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java new file mode 100644 index 0000000000..dd3b51fc5d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java @@ -0,0 +1,365 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; +import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; +import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EventInfo; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.event.EventType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +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.service.security.permission.Operation; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; +import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_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.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class AlarmRuleController extends BaseController { + + private final TbCalculatedFieldService tbCalculatedFieldService; + private final EventService eventService; + private final TbelInvokeService tbelInvokeService; + + public static final String ALARM_RULE_ID = "alarmRuleId"; + + public static final int TIMEOUT = 20; + + private static final String TEST_SCRIPT_EXPRESSION = + "Execute the Script expression and return the result. The format of request: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + + " \"arguments\": {\n" + + " \"temperature\": {\n" + + " \"type\": \"TS_ROLLING\",\n" + + " \"timeWindow\": {\n" + + " \"startTs\": 1739775630002,\n" + + " \"endTs\": 65432211,\n" + + " \"limit\": 5\n" + + " },\n" + + " \"values\": [\n" + + " { \"ts\": 1739775639851, \"value\": 23 },\n" + + " { \"ts\": 1739775664561, \"value\": 43 },\n" + + " { \"ts\": 1739775713079, \"value\": 15 },\n" + + " { \"ts\": 1739775999522, \"value\": 34 },\n" + + " { \"ts\": 1739776228452, \"value\": 22 }\n" + + " ]\n" + + " },\n" + + " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Expected result JSON contains \"output\" and \"error\"."; + + @ApiOperation(value = "Create Or Update Alarm Rule (saveAlarmRule)", + notes = "Creates or Updates the Alarm Rule. When creating alarm rule, platform generates Alarm Rule Id as " + UUID_WIKI_LINK + + "The newly created Alarm Rule Id will be present in the response. " + + "Specify existing Alarm Rule Id to update the alarm rule. " + + "Referencing non-existing Alarm Rule Id will cause 'Not Found' error. " + + "Remove 'id', 'tenantId' from the request body example (below) to create new Alarm Rule entity. " + + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/alarm/rule") + public AlarmRuleDefinition saveAlarmRule(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm rule.") + @RequestBody AlarmRuleDefinition alarmRuleDefinition) throws Exception { + alarmRuleDefinition.setTenantId(getTenantId()); + checkEntityId(alarmRuleDefinition.getEntityId(), Operation.WRITE_CALCULATED_FIELD); + CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField(); + checkReferencedEntities(calculatedField.getConfiguration()); + CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser()); + return AlarmRuleDefinition.fromCalculatedField(saved); + } + + @ApiOperation(value = "Get Alarm Rule (getAlarmRuleById)", + notes = "Fetch the Alarm Rule object based on the provided Alarm Rule Id." + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rule/{alarmRuleId}") + public AlarmRuleDefinition getAlarmRuleById(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + checkNotNull(calculatedField); + checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); + return AlarmRuleDefinition.fromCalculatedField(calculatedField); + } + + @ApiOperation(value = "Get Alarm Rules by Entity Id (getAlarmRulesByEntityId)", + notes = "Fetch the Alarm Rules based on the provided Entity Id." + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/alarm/rule/{entityType}/{entityId}") + public PageData getAlarmRulesByEntityId( + @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + checkParameter("entityId", entityIdStr); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); + checkEntityId(entityId, Operation.READ_CALCULATED_FIELD); + PageData result = checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, CalculatedFieldType.ALARM, pageLink)); + return result.mapData(AlarmRuleDefinition::fromCalculatedField); + } + + @ApiOperation(value = "Get alarm rules (getAlarmRules)", + notes = "Fetch tenant alarm rules based on the filter.") + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/alarm/rules") + public PageData getAlarmRules(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Entity type filter. If not specified, alarm rules for all supported entity types will be returned.") + @RequestParam(required = false) EntityType entityType, + @Parameter(description = "Entities filter. If not specified, alarm rules for entity type filter will be returned.") + @RequestParam(required = false) Set entities, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + SecurityUser user = getCurrentUser(); + + Set types = EnumSet.of(CalculatedFieldType.ALARM); + + Set entityTypes; + if (entityType == null) { + entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream() + .filter(entry -> CollectionUtils.containsAny(entry.getValue(), types)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } else { + entityTypes = EnumSet.of(entityType); + } + + CalculatedFieldFilter filter = CalculatedFieldFilter.builder() + .types(types) + .entityTypes(entityTypes) + .entityIds(entities) + .build(); + PageData result = calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink); + return result.mapData(AlarmRuleDefinitionInfo::fromCalculatedFieldInfo); + } + + @ApiOperation(value = "Get alarm rule names (getAlarmRuleNames)", + notes = "Fetch the list of alarm rule names.") + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/alarm/rules/names") + public PageData getAlarmRuleNames(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, "name", sortOrder); + return calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType(getTenantId(), CalculatedFieldType.ALARM, pageLink); + } + + @ApiOperation(value = "Delete Alarm Rule (deleteAlarmRule)", + notes = "Deletes the alarm rule. Referencing non-existing Alarm Rule Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @DeleteMapping("/alarm/rule/{alarmRuleId}") + @ResponseStatus(HttpStatus.OK) + public void deleteAlarmRule(@PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws Exception { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); + tbCalculatedFieldService.delete(calculatedField, getCurrentUser()); + } + + @ApiOperation(value = "Get latest alarm rule debug event (getLatestAlarmRuleDebugEvent)", + notes = "Gets latest alarm rule debug event for specified alarm rule id. " + + "Referencing non-existing alarm rule id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rule/{alarmRuleId}/debug") + public JsonNode getLatestAlarmRuleDebugEvent(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); + TenantId tenantId = getCurrentUser().getTenantId(); + return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1)) + .flatMap(events -> events.stream().map(EventInfo::getBody).findFirst()) + .orElse(null); + } + + @ApiOperation(value = "Test Script expression", + notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/alarm/rule/testScript") + public JsonNode testAlarmRuleScript( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL expression.") + @RequestBody JsonNode inputParams) { + String expression = inputParams.get("expression").asText(); + Map arguments = Objects.requireNonNullElse( + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), + Collections.emptyMap() + ); + + ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); + ctxAndArgNames.add("ctx"); + ctxAndArgNames.addAll(arguments.keySet()); + + String output = ""; + String errorText = ""; + + CalculatedFieldTbelScriptEngine engine = null; + try { + if (tbelInvokeService == null) { + throw new IllegalArgumentException("TBEL script engine is disabled!"); + } + + engine = new CalculatedFieldTbelScriptEngine( + getTenantId(), + tbelInvokeService, + expression, + ctxAndArgNames.toArray(String[]::new) + ); + + Object[] args = new Object[ctxAndArgNames.size()]; + args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); + for (int i = 1; i < ctxAndArgNames.size(); i++) { + var arg = arguments.get(ctxAndArgNames.get(i)); + if (arg instanceof TbelCfSingleValueArg svArg) { + args[i] = svArg.getValue(); + } else { + args[i] = arg; + } + } + + JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); + output = JacksonUtil.toString(json); + } catch (Exception e) { + log.error("Error evaluating expression", e); + Throwable rootCause = ExceptionUtils.getRootCause(e); + errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); + } finally { + if (engine != null) { + engine.destroy(); + } + } + return JacksonUtil.newObjectNode() + .put("output", output) + .put("error", errorText); + } + + private long getLatestTimestamp(Map arguments) { + long lastUpdateTimestamp = -1; + for (TbelCfArg entry : arguments.values()) { + if (entry instanceof TbelCfSingleValueArg singleValueArg) { + long ts = singleValueArg.getTs(); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); + } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { + long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); + } + } + return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; + } + + private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { + Set referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); + for (EntityId referencedEntityId : referencedEntityIds) { + EntityType entityTypeVal = referencedEntityId.getEntityType(); + switch (entityTypeVal) { + case TENANT -> { + return; + } + case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); + default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityTypeVal + "' for referenced entities."); + } + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index cf94940e07..7041a71086 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -48,8 +48,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.predic import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate.StringOperation; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; @@ -128,32 +127,32 @@ public class AlarmRulesTest extends AbstractControllerTest { ); Condition clearRule = new Condition("return temperature <= 25;", null, null); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, clearRule); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); postTelemetry(deviceId, "{\"temperature\":100}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isSeverityUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); postTelemetry(deviceId, "{\"temperature\":101}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); postTelemetry(deviceId, "{\"temperature\":20}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCleared()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); @@ -185,11 +184,11 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition(simpleExpression, null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":100}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -209,11 +208,11 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"values\": {\"temperature\": 50}, \"ts\": " + (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30) + "}")); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -236,15 +235,15 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); for (int i = 0; i < 4; i++) { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(10); } - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -255,12 +254,12 @@ public class AlarmRulesTest extends AbstractControllerTest { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(10); } - checkAlarmResult(calculatedField, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> { assertThat(alarmResult.isUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); }); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isSeverityUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -288,14 +287,14 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue<>(null, "eventsCount"), null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"eventsCount\":" + eventsCount + "}"); for (int i = 0; i < eventsCount; i++) { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(10); } - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -319,13 +318,13 @@ public class AlarmRulesTest extends AbstractControllerTest { ); Condition clearRule = new Condition("return powerConsumption < 3000;", null, clearDurationMs); - CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 5 seconds", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 5 seconds", arguments, createRules, clearRule); postTelemetry(deviceId, "{\"powerConsumption\":3500}"); Thread.sleep(createDurationMs - 2000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -334,9 +333,9 @@ public class AlarmRulesTest extends AbstractControllerTest { postTelemetry(deviceId, "{\"powerConsumption\":2000}"); Thread.sleep(clearDurationMs - 2000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCleared()).isTrue(); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); assertThat(alarmResult.getConditionDuration()).isBetween(clearDurationMs, clearDurationMs + 2000); @@ -363,12 +362,12 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue(null, "duration"), null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds", arguments, createRules, null); postTelemetry(deviceId, "{\"powerConsumption\":3500}"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"duration\":" + createDurationMs + "}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -391,10 +390,10 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue(2000L, null), null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds", arguments, createRules, null); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -424,12 +423,12 @@ public class AlarmRulesTest extends AbstractControllerTest { device.setCustomerId(customerId); device = doPost("/api/device", device, Device.class); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}"); postTelemetry(deviceId, "{\"temperature\":51}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -462,21 +461,21 @@ public class AlarmRulesTest extends AbstractControllerTest { "location", StringOperation.NOT_CONTAINS, new AlarmConditionValue<>(null, "locationFilter") ), null, null); - CalculatedField calculatedField = createAlarmCf(customerId, "New resident", + AlarmRuleDefinition alarmRule = createAlarmRule(customerId, "New resident", arguments, createRules, clearRule); loginSysAdmin(); postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"locationFilter\":\"Kyiv\"}"); loginTenantAdmin(); postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Kyiv\"}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Lviv\"}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCleared()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); @@ -501,7 +500,7 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue<>(null, "schedule")) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); String schedule = """ {"timezone":"Europe/Kiev","items":[{"enabled":false,"dayOfWeek":1,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":2,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":3,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":4,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":5,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":6,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":7,"startsOn":0,"endsOn":0}]} @@ -510,14 +509,14 @@ public class AlarmRulesTest extends AbstractControllerTest { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(1000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); schedule = schedule.replace("\"enabled\":false", "\"enabled\":true"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { // checking multiple debug events due to scheduled reevaluation (which also produces debug events) - CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream() + CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream() .filter(event -> event.getResult() != null) .findFirst().orElse(null); assertThat(debugEvent).isNotNull(); @@ -565,18 +564,18 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition(criticalExpression, null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "No Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "No Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isSeverityUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -596,19 +595,19 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); - calculatedField.setName("New alarm type"); - calculatedField = saveCalculatedField(calculatedField); - checkAlarmResult(calculatedField, alarmResult -> { + alarmRule.setName("New alarm type"); + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -628,18 +627,18 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(1000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); - AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration(); ((TbelAlarmConditionExpression) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition().getExpression()) .setExpression("return temperature >= 50;"); - calculatedField = saveCalculatedField(calculatedField); - checkAlarmResult(calculatedField, alarmResult -> { + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -662,13 +661,13 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); for (int i = 0; i < eventsCountMajor; i++) { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(10); } - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -676,18 +675,18 @@ public class AlarmRulesTest extends AbstractControllerTest { }); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getConditionRepeats()).isEqualTo(6); }); // decreasing required events count for critical rule - AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration(); ((RepeatingAlarmCondition) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition()) .setCount(new AlarmConditionValue<>(6, null)); - calculatedField = saveCalculatedField(calculatedField); - checkAlarmResult(calculatedField, alarmResult -> { + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isSeverityUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -719,20 +718,20 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(1000); // not created because tenant's threshold 100 is used - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); - ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getArguments().get("temperatureThreshold") + ((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getArguments().get("temperatureThreshold") .setRefDynamicSourceConfiguration(null); // using threshold 50 on device level - calculatedField = saveCalculatedField(calculatedField); + alarmRule = saveAlarmRule(alarmRule); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -755,7 +754,7 @@ public class AlarmRulesTest extends AbstractControllerTest { Map createRules = Map.of( AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature and Humidity Alarm", arguments, createRules, null, configuration -> { configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( "temperature is ${temperature}, humidity is ${humidity}" @@ -765,18 +764,18 @@ public class AlarmRulesTest extends AbstractControllerTest { postTelemetry(deviceId, "{\"temperature\":50}"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) .isEqualTo("temperature is 50, humidity is 50"); }); - ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( + ((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( "UPDATED temperature is ${temperature}, humidity is ${humidity}" ); - calculatedField = saveCalculatedField(calculatedField); - checkAlarmResult(calculatedField, alarmResult -> { + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isFalse(); assertThat(alarmResult.isUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); @@ -805,16 +804,16 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue<>(schedule, null)) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "Illegal parking alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "Illegal parking alarm", arguments, createRules, null); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"parkingSpotOccupied\":true}"); Thread.sleep(10000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream() + CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream() .filter(event -> event.getResult() != null) .findFirst().orElse(null); assertThat(debugEvent).isNotNull(); @@ -838,11 +837,11 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); - Alarm alarm = checkAlarmResult(calculatedField, alarmResult -> { + Alarm alarm = checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -851,7 +850,7 @@ public class AlarmRulesTest extends AbstractControllerTest { doPost("/api/alarm/" + alarm.getId() + "/clear", AlarmInfo.class); Thread.sleep(1000); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.getAlarm().getId()).isNotEqualTo(alarm.getId()); assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); @@ -861,21 +860,21 @@ public class AlarmRulesTest extends AbstractControllerTest { // TODO: MSA tests - private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, Consumer assertion) { - return checkAlarmResult(calculatedField, null, assertion); + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer assertion) { + return checkAlarmResult(alarmRule, null, assertion); } - private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Predicate waitFor, Consumer assertion) { TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> getLatestAlarmResult(calculatedField.getId()), result -> + .until(() -> getLatestAlarmResult(alarmRule.getId()), result -> result != null && (waitFor == null || waitFor.test(result))); assertion.accept(alarmResult); Alarm alarm = alarmResult.getAlarm(); assertThat(alarm.getOriginator()).isEqualTo(originatorId); - assertThat(alarm.getType()).isEqualTo(calculatedField.getName()); + assertThat(alarm.getType()).isEqualTo(alarmRule.getName()); return alarmResult; } @@ -895,38 +894,37 @@ public class AlarmRulesTest extends AbstractControllerTest { return JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); } - private CalculatedField createAlarmCf(EntityId entityId, - String alarmType, - Map arguments, - Map createConditions, - Condition clearCondition, - Consumer... modifier) { + private AlarmRuleDefinition createAlarmRule(EntityId entityId, + String alarmType, + Map arguments, + Map createConditions, + Condition clearCondition, + Consumer... modifier) { Map createRules = new HashMap<>(); createConditions.forEach((severity, condition) -> { createRules.put(severity, toAlarmRule(condition)); }); AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; - CalculatedField calculatedField = new CalculatedField(); - calculatedField.setEntityId(entityId); - calculatedField.setName(alarmType); - calculatedField.setType(CalculatedFieldType.ALARM); + AlarmRuleDefinition alarmRule = new AlarmRuleDefinition(); + alarmRule.setEntityId(entityId); + alarmRule.setName(alarmType); AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); configuration.setArguments(arguments); configuration.setCreateRules(createRules); configuration.setClearRule(clearRule); - calculatedField.setConfiguration(configuration); - calculatedField.setDebugSettings(DebugSettings.all()); + alarmRule.setConfiguration(configuration); + alarmRule.setDebugSettings(DebugSettings.all()); if (modifier.length > 0) { modifier[0].accept(configuration); } - CalculatedField savedCalculatedField = saveCalculatedField(calculatedField); + AlarmRuleDefinition savedAlarmRule = saveAlarmRule(alarmRule); CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> getDebugEvents(savedCalculatedField.getId(), 1), + .until(() -> getDebugEvents(savedAlarmRule.getId(), 1), events -> !events.isEmpty()).get(0); latestEventId = debugEvent.getId(); - return savedCalculatedField; + return savedAlarmRule; } private AlarmRule toAlarmRule(Condition condition) { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 6a658ffd66..055e1e19a0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -94,6 +94,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -1482,6 +1483,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doPost("/api/calculatedField", calculatedField, CalculatedField.class); } + protected AlarmRuleDefinition saveAlarmRule(AlarmRuleDefinition alarmRule) { + return doPost("/api/alarm/rule", alarmRule, AlarmRuleDefinition.class); + } + protected PageData getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java new file mode 100644 index 0000000000..67b743b3bb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java @@ -0,0 +1,165 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasAdditionalInfo; +import org.thingsboard.server.common.data.HasDebugSettings; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +@Schema +@Data +@EqualsAndHashCode(callSuper = true) +public class AlarmRuleDefinition extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo { + + private TenantId tenantId; + private EntityId entityId; + + @NoXss + @Length(fieldName = "name") + @Schema(description = "User defined name of the alarm rule.") + private String name; + @Deprecated + @Schema(description = "Enable/disable debug. ", example = "false", deprecated = true) + private boolean debugMode; + @Schema(description = "Debug settings object.") + private DebugSettings debugSettings; + @Schema(description = "Version of alarm rule configuration.", example = "0") + private int configurationVersion; + @Schema(implementation = AlarmCalculatedFieldConfiguration.class) + @Valid + @NotNull + private AlarmCalculatedFieldConfiguration configuration; + private Long version; + @NoXss + @Schema(description = "Additional parameters of the alarm rule") + private JsonNode additionalInfo; + + public AlarmRuleDefinition() {} + + public AlarmRuleDefinition(CalculatedFieldId id) { + super(id); + } + + public AlarmRuleDefinition(AlarmRuleDefinition alarmRuleDefinition) { + super(alarmRuleDefinition); + this.tenantId = alarmRuleDefinition.tenantId; + this.entityId = alarmRuleDefinition.entityId; + this.name = alarmRuleDefinition.name; + this.debugMode = alarmRuleDefinition.debugMode; + this.debugSettings = alarmRuleDefinition.debugSettings; + this.configurationVersion = alarmRuleDefinition.configurationVersion; + this.configuration = alarmRuleDefinition.configuration; + this.version = alarmRuleDefinition.version; + this.additionalInfo = alarmRuleDefinition.additionalInfo; + } + + @Schema(description = "JSON object with the Alarm Rule Id. Referencing non-existing Alarm Rule Id will cause error.") + @Override + public CalculatedFieldId getId() { + return super.getId(); + } + + @Schema(description = "Timestamp of the alarm rule creation, in milliseconds", example = "1609459200000", accessMode = Schema.AccessMode.READ_ONLY) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + // Getter is ignored for serialization + @JsonIgnore + public boolean isDebugMode() { + return debugMode; + } + + // Setter is annotated for deserialization + @JsonSetter + public void setDebugMode(boolean debugMode) { + this.debugMode = debugMode; + } + + public EntityType getEntityType() { + return EntityType.CALCULATED_FIELD; + } + + public CalculatedField toCalculatedField() { + CalculatedField cf = new CalculatedField(); + cf.setId(this.id); + cf.setCreatedTime(this.createdTime); + cf.setTenantId(this.tenantId); + cf.setEntityId(this.entityId); + cf.setType(CalculatedFieldType.ALARM); + cf.setName(this.name); + cf.setDebugMode(this.debugMode); + cf.setDebugSettings(this.debugSettings); + cf.setConfigurationVersion(this.configurationVersion); + cf.setConfiguration(this.configuration); + cf.setVersion(this.version); + cf.setAdditionalInfo(this.additionalInfo); + return cf; + } + + public static AlarmRuleDefinition fromCalculatedField(CalculatedField cf) { + AlarmRuleDefinition def = new AlarmRuleDefinition(); + def.setId(cf.getId()); + def.setCreatedTime(cf.getCreatedTime()); + def.setTenantId(cf.getTenantId()); + def.setEntityId(cf.getEntityId()); + def.setName(cf.getName()); + def.setDebugMode(cf.isDebugMode()); + def.setDebugSettings(cf.getDebugSettings()); + def.setConfigurationVersion(cf.getConfigurationVersion()); + def.setConfiguration((AlarmCalculatedFieldConfiguration) cf.getConfiguration()); + def.setVersion(cf.getVersion()); + def.setAdditionalInfo(cf.getAdditionalInfo()); + return def; + } + + @Override + public String toString() { + return new StringBuilder() + .append("AlarmRuleDefinition[") + .append("tenantId=").append(tenantId) + .append(", entityId=").append(entityId) + .append(", name='").append(name) + .append(", configurationVersion=").append(configurationVersion) + .append(", configuration=").append(configuration) + .append(", additionalInfo=").append(additionalInfo) + .append(", version=").append(version) + .append(", createdTime=").append(createdTime) + .append(", id=").append(id).append(']') + .toString(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java new file mode 100644 index 0000000000..145dfc4081 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class AlarmRuleDefinitionInfo extends AlarmRuleDefinition { + + private String entityName; + + public AlarmRuleDefinitionInfo(AlarmRuleDefinition alarmRuleDefinition, String entityName) { + super(alarmRuleDefinition); + this.entityName = entityName; + } + + public static AlarmRuleDefinitionInfo fromCalculatedFieldInfo(CalculatedFieldInfo cfi) { + AlarmRuleDefinition def = AlarmRuleDefinition.fromCalculatedField(cfi); + return new AlarmRuleDefinitionInfo(def, cfi.getEntityName()); + } + +} diff --git a/docs/superpowers/plans/2026-03-30-alarm-rule-controller.md b/docs/superpowers/plans/2026-03-30-alarm-rule-controller.md new file mode 100644 index 0000000000..af113ceaf1 --- /dev/null +++ b/docs/superpowers/plans/2026-03-30-alarm-rule-controller.md @@ -0,0 +1,985 @@ +# Alarm Rule Controller Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Create a dedicated AlarmRuleController REST API that wraps the CalculatedField service layer, hiding the CF implementation detail from alarm rule API consumers. + +**Architecture:** Thin controller wrapper. New `AlarmRuleDefinition` and `AlarmRuleDefinitionInfo` DTOs wrap `CalculatedField`/`CalculatedFieldInfo`. `AlarmRuleController` delegates to `TbCalculatedFieldService` and `CalculatedFieldService` (DAO). UI gets a new `AlarmRulesService` pointing at the new endpoints. `AlarmRulesTest` switches to the new API. + +**Tech Stack:** Java 17+, Spring Boot, Angular 17+, JUnit 4 / Spring MockMvc + +--- + +## File Structure + +**Backend (new):** +- `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java` — DTO wrapping CalculatedField without `type` +- `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java` — extends AlarmRuleDefinition, adds `entityName` +- `application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java` — REST controller + +**Backend (modify):** +- `application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java:1652` — add `saveAlarmRule()` helper alongside existing `saveCalculatedField()` +- `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java` — switch from CF API to alarm rule API + +**Frontend (new):** +- `ui-ngx/src/app/core/http/alarm-rules.service.ts` — Angular HTTP service for alarm rule endpoints + +**Frontend (modify):** +- `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts` — switch from `CalculatedFieldsService` to `AlarmRulesService` +- `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts` — inject `AlarmRulesService` instead of `CalculatedFieldsService` +- `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts` — use `AlarmRulesService` for all CRUD operations + +--- + +### Task 1: Create AlarmRuleDefinition DTO + +**Files:** +- Create: `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java` + +- [ ] **Step 1: Create AlarmRuleDefinition class** + +```java +package org.thingsboard.server.common.data.cf; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasAdditionalInfo; +import org.thingsboard.server.common.data.HasDebugSettings; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.TenantEntity; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +@Schema +@Data +@EqualsAndHashCode(callSuper = true) +public class AlarmRuleDefinition extends BaseData implements HasName, TenantEntity, HasVersion, HasDebugSettings, HasAdditionalInfo { + + private TenantId tenantId; + private EntityId entityId; + + @NoXss + @Length(fieldName = "name") + @Schema(description = "Alarm type name.") + private String name; + @Deprecated + @Schema(description = "Enable/disable debug.", example = "false", deprecated = true) + private boolean debugMode; + @Schema(description = "Debug settings object.") + private DebugSettings debugSettings; + @Schema(description = "Version of alarm rule configuration.", example = "0") + private int configurationVersion; + @Schema(implementation = AlarmCalculatedFieldConfiguration.class) + @Valid + @NotNull + private AlarmCalculatedFieldConfiguration configuration; + private Long version; + @NoXss + @Schema(description = "Additional parameters of the alarm rule") + private JsonNode additionalInfo; + + public AlarmRuleDefinition() {} + + public AlarmRuleDefinition(CalculatedFieldId id) { + super(id); + } + + public AlarmRuleDefinition(AlarmRuleDefinition other) { + super(other); + this.tenantId = other.tenantId; + this.entityId = other.entityId; + this.name = other.name; + this.debugMode = other.debugMode; + this.debugSettings = other.debugSettings; + this.configurationVersion = other.configurationVersion; + this.configuration = other.configuration; + this.version = other.version; + this.additionalInfo = other.additionalInfo; + } + + @Schema(description = "JSON object with the Alarm Rule Id.") + @Override + public CalculatedFieldId getId() { + return super.getId(); + } + + @Schema(description = "Timestamp of the alarm rule creation, in milliseconds", example = "1609459200000", accessMode = Schema.AccessMode.READ_ONLY) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @JsonIgnore + public boolean isDebugMode() { + return debugMode; + } + + @JsonSetter + public void setDebugMode(boolean debugMode) { + this.debugMode = debugMode; + } + + @Override + public EntityType getEntityType() { + return EntityType.CALCULATED_FIELD; + } + + public CalculatedField toCalculatedField() { + CalculatedField cf = new CalculatedField(); + cf.setId(this.getId()); + cf.setCreatedTime(this.getCreatedTime()); + cf.setTenantId(this.tenantId); + cf.setEntityId(this.entityId); + cf.setType(CalculatedFieldType.ALARM); + cf.setName(this.name); + cf.setDebugMode(this.debugMode); + cf.setDebugSettings(this.debugSettings); + cf.setConfigurationVersion(this.configurationVersion); + cf.setConfiguration(this.configuration); + cf.setVersion(this.version); + cf.setAdditionalInfo(this.additionalInfo); + return cf; + } + + public static AlarmRuleDefinition fromCalculatedField(CalculatedField cf) { + AlarmRuleDefinition def = new AlarmRuleDefinition(); + def.setId(cf.getId()); + def.setCreatedTime(cf.getCreatedTime()); + def.setTenantId(cf.getTenantId()); + def.setEntityId(cf.getEntityId()); + def.setName(cf.getName()); + def.setDebugMode(cf.isDebugMode()); + def.setDebugSettings(cf.getDebugSettings()); + def.setConfigurationVersion(cf.getConfigurationVersion()); + def.setConfiguration((AlarmCalculatedFieldConfiguration) cf.getConfiguration()); + def.setVersion(cf.getVersion()); + def.setAdditionalInfo(cf.getAdditionalInfo()); + return def; + } +} +``` + +- [ ] **Step 2: Verify compilation** + +Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn compile -pl common/data -am -q -DskipTests 2>&1 | tail -5` +Expected: BUILD SUCCESS + +- [ ] **Step 3: Commit** + +```bash +git add common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java +git commit -m "Add AlarmRuleDefinition DTO" +``` + +--- + +### Task 2: Create AlarmRuleDefinitionInfo DTO + +**Files:** +- Create: `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java` + +- [ ] **Step 1: Create AlarmRuleDefinitionInfo class** + +```java +package org.thingsboard.server.common.data.cf; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class AlarmRuleDefinitionInfo extends AlarmRuleDefinition { + + private String entityName; + + public AlarmRuleDefinitionInfo(AlarmRuleDefinition alarmRuleDefinition, String entityName) { + super(alarmRuleDefinition); + this.entityName = entityName; + } + + public static AlarmRuleDefinitionInfo fromCalculatedFieldInfo(CalculatedFieldInfo cfi) { + AlarmRuleDefinitionInfo info = new AlarmRuleDefinitionInfo(); + AlarmRuleDefinition def = AlarmRuleDefinition.fromCalculatedField(cfi); + info.setId(def.getId()); + info.setCreatedTime(def.getCreatedTime()); + info.setTenantId(def.getTenantId()); + info.setEntityId(def.getEntityId()); + info.setName(def.getName()); + info.setDebugMode(def.isDebugMode()); + info.setDebugSettings(def.getDebugSettings()); + info.setConfigurationVersion(def.getConfigurationVersion()); + info.setConfiguration(def.getConfiguration()); + info.setVersion(def.getVersion()); + info.setAdditionalInfo(def.getAdditionalInfo()); + info.setEntityName(cfi.getEntityName()); + return info; + } +} +``` + +- [ ] **Step 2: Verify compilation** + +Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn compile -pl common/data -am -q -DskipTests 2>&1 | tail -5` +Expected: BUILD SUCCESS + +- [ ] **Step 3: Commit** + +```bash +git add common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java +git commit -m "Add AlarmRuleDefinitionInfo DTO" +``` + +--- + +### Task 3: Create AlarmRuleController + +**Files:** +- Create: `application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java` + +- [ ] **Step 1: Create the controller with all endpoints** + +```java +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; +import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; +import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EventInfo; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.event.EventType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +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.permission.Operation; +import org.thingsboard.server.common.data.permission.Resource; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; +import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_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.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class AlarmRuleController extends BaseController { + + private final TbCalculatedFieldService tbCalculatedFieldService; + private final EventService eventService; + private final TbelInvokeService tbelInvokeService; + + private static final String ALARM_RULE_ID = "alarmRuleId"; + private static final int TIMEOUT = 20; + + private static final String TEST_SCRIPT_EXPRESSION = + "Execute the Script expression and return the result. The format of request: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + + " \"arguments\": {\n" + + " \"temperature\": {\n" + + " \"type\": \"TS_ROLLING\",\n" + + " \"timeWindow\": {\n" + + " \"startTs\": 1739775630002,\n" + + " \"endTs\": 65432211,\n" + + " \"limit\": 5\n" + + " },\n" + + " \"values\": [\n" + + " { \"ts\": 1739775639851, \"value\": 23 },\n" + + " { \"ts\": 1739775664561, \"value\": 43 },\n" + + " { \"ts\": 1739775713079, \"value\": 15 },\n" + + " { \"ts\": 1739775999522, \"value\": 34 },\n" + + " { \"ts\": 1739776228452, \"value\": 22 }\n" + + " ]\n" + + " },\n" + + " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Expected result JSON contains \"output\" and \"error\"."; + + @ApiOperation(value = "Create Or Update Alarm Rule (saveAlarmRule)", + notes = "Creates or Updates the Alarm Rule. When creating, platform generates Alarm Rule Id as " + UUID_WIKI_LINK + + "The newly created Alarm Rule Id will be present in the response. " + + "Specify existing Alarm Rule Id to update. " + + "Referencing non-existing Alarm Rule Id will cause 'Not Found' error. " + + "Remove 'id', 'tenantId' from the request body example (below) to create new Alarm Rule entity. " + + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/alarm/rule") + public AlarmRuleDefinition saveAlarmRule( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm rule.") + @RequestBody AlarmRuleDefinition alarmRuleDefinition) throws Exception { + alarmRuleDefinition.setTenantId(getTenantId()); + CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField(); + checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); + checkReferencedEntities(calculatedField); + CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser()); + return AlarmRuleDefinition.fromCalculatedField(saved); + } + + @ApiOperation(value = "Get Alarm Rule (getAlarmRuleById)", + notes = "Fetch the Alarm Rule object based on the provided Alarm Rule Id." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rule/{alarmRuleId}") + public AlarmRuleDefinition getAlarmRuleById( + @Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + checkNotNull(calculatedField); + checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); + return AlarmRuleDefinition.fromCalculatedField(calculatedField); + } + + @ApiOperation(value = "Get Alarm Rules by Entity Id (getAlarmRulesByEntityId)", + notes = "Fetch the Alarm Rules based on the provided Entity Id." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rule/{entityType}/{entityId}") + public PageData getAlarmRulesByEntityId( + @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = "Filter by alarm rule name.") @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + checkParameter("entityId", entityIdStr); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); + checkEntityId(entityId, Operation.READ_CALCULATED_FIELD); + PageData result = checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, CalculatedFieldType.ALARM, pageLink)); + return result.mapData(AlarmRuleDefinition::fromCalculatedField); + } + + @ApiOperation(value = "Get alarm rules (getAlarmRules)", + notes = "Fetch tenant alarm rules based on the filter." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rules") + public PageData getAlarmRules( + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = "Entity type filter.") @RequestParam(required = false) EntityType entityType, + @Parameter(description = "Entities filter.") @RequestParam(required = false) Set entities, + @Parameter(description = "Filter by alarm rule name.") @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + SecurityUser user = getCurrentUser(); + + Set types = EnumSet.of(CalculatedFieldType.ALARM); + + Set entityTypes; + if (entityType == null) { + entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream() + .filter(entry -> entry.getValue().contains(CalculatedFieldType.ALARM)) + .map(Map.Entry::getKey) + .filter(t -> { + try { + return accessControlService.hasPermission(user, Resource.resourceFromEntityType(t), Operation.READ_CALCULATED_FIELD); + } catch (ThingsboardException e) { + return false; + } + }) + .collect(Collectors.toSet()); + } else { + accessControlService.checkPermission(user, Resource.resourceFromEntityType(entityType), Operation.READ_CALCULATED_FIELD); + entityTypes = EnumSet.of(entityType); + } + + CalculatedFieldFilter filter = CalculatedFieldFilter.builder() + .types(types) + .entityTypes(entityTypes) + .entityIds(entities) + .build(); + PageData result = calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink); + return result.mapData(AlarmRuleDefinitionInfo::fromCalculatedFieldInfo); + } + + @ApiOperation(value = "Get alarm rule names (getAlarmRuleNames)", + notes = "Fetch the list of alarm rule names." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rules/names") + public PageData getAlarmRuleNames( + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = "Filter by alarm rule name.") @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, "name", sortOrder); + return calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType(getTenantId(), CalculatedFieldType.ALARM, pageLink); + } + + @ApiOperation(value = "Delete Alarm Rule (deleteAlarmRule)", + notes = "Deletes the alarm rule. Referencing non-existing Alarm Rule Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @DeleteMapping("/alarm/rule/{alarmRuleId}") + @ResponseStatus(HttpStatus.OK) + public void deleteAlarmRule(@PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws Exception { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); + tbCalculatedFieldService.delete(calculatedField, getCurrentUser()); + } + + @ApiOperation(value = "Get latest alarm rule debug event (getLatestAlarmRuleDebugEvent)", + notes = "Gets latest alarm rule debug event for specified alarm rule id. " + + "Referencing non-existing alarm rule id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rule/{alarmRuleId}/debug") + public JsonNode getLatestAlarmRuleDebugEvent( + @Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); + TenantId tenantId = getCurrentUser().getTenantId(); + return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1)) + .flatMap(events -> events.stream().map(EventInfo::getBody).findFirst()) + .orElse(null); + } + + @ApiOperation(value = "Test Script expression", + notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/alarm/rule/testScript") + public JsonNode testAlarmRuleScript( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL expression.") + @RequestBody JsonNode inputParams) { + String expression = inputParams.get("expression").asText(); + Map arguments = Objects.requireNonNullElse( + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), + Collections.emptyMap() + ); + + ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); + ctxAndArgNames.add("ctx"); + ctxAndArgNames.addAll(arguments.keySet()); + + String output = ""; + String errorText = ""; + + CalculatedFieldTbelScriptEngine engine = null; + try { + if (tbelInvokeService == null) { + throw new IllegalArgumentException("TBEL script engine is disabled!"); + } + + engine = new CalculatedFieldTbelScriptEngine( + getTenantId(), + tbelInvokeService, + expression, + ctxAndArgNames.toArray(String[]::new) + ); + + Object[] args = new Object[ctxAndArgNames.size()]; + args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); + for (int i = 1; i < ctxAndArgNames.size(); i++) { + var arg = arguments.get(ctxAndArgNames.get(i)); + if (arg instanceof TbelCfSingleValueArg svArg) { + args[i] = svArg.getValue(); + } else { + args[i] = arg; + } + } + + JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); + output = JacksonUtil.toString(json); + } catch (Exception e) { + log.error("Error evaluating expression", e); + Throwable rootCause = ExceptionUtils.getRootCause(e); + errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); + } finally { + if (engine != null) { + engine.destroy(); + } + } + return JacksonUtil.newObjectNode() + .put("output", output) + .put("error", errorText); + } + + private long getLatestTimestamp(Map arguments) { + long lastUpdateTimestamp = -1; + for (TbelCfArg entry : arguments.values()) { + if (entry instanceof TbelCfSingleValueArg singleValueArg) { + long ts = singleValueArg.getTs(); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); + } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { + long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); + } + } + return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; + } + + private void checkReferencedEntities(CalculatedField calculatedField) throws ThingsboardException { + Set referencedEntityIds = calculatedField.getConfiguration().getReferencedEntities(); + for (EntityId referencedEntityId : referencedEntityIds) { + EntityType refEntityType = referencedEntityId.getEntityType(); + switch (refEntityType) { + case TENANT -> { + return; + } + case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); + default -> throw new IllegalArgumentException("Alarm rules do not support '" + refEntityType + "' for referenced entities."); + } + } + } +} +``` + +- [ ] **Step 2: Check that `PageData.mapData` exists** + +The controller uses `result.mapData(...)` to convert page results. Verify this method exists: + +Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && grep -n 'mapData' common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java` + +If `mapData` doesn't exist, replace with manual conversion: +```java +new PageData<>(result.getData().stream().map(AlarmRuleDefinition::fromCalculatedField).toList(), result.getTotalPages(), result.getTotalElements(), result.hasNext()) +``` + +- [ ] **Step 3: Verify compilation** + +Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn compile -pl application -am -q -DskipTests 2>&1 | tail -10` +Expected: BUILD SUCCESS + +- [ ] **Step 4: Commit** + +```bash +git add application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java +git commit -m "Add AlarmRuleController with all endpoints" +``` + +--- + +### Task 4: Update AlarmRulesTest to use new API + +**Files:** +- Modify: `application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java:1652` +- Modify: `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java` + +- [ ] **Step 1: Add `saveAlarmRule` helper to AbstractWebTest** + +In `application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java`, after the existing `saveCalculatedField` method at line 1652-1654, add: + +```java + protected AlarmRuleDefinition saveAlarmRule(AlarmRuleDefinition alarmRule) { + return doPost("/api/alarm/rule", alarmRule, AlarmRuleDefinition.class); + } +``` + +Also add the necessary import at the top of AbstractWebTest.java: +```java +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +``` + +- [ ] **Step 2: Update AlarmRulesTest imports** + +In `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java`, add the import: + +```java +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +``` + +- [ ] **Step 3: Update `createAlarmCf` to `createAlarmRule` using new DTO** + +Replace the `createAlarmCf` method (starting at line 955) with: + +```java + private AlarmRuleDefinition createAlarmRule(EntityId entityId, + String alarmType, + Map arguments, + Map createConditions, + Condition clearCondition, + Consumer... modifier) { + Map createRules = new HashMap<>(); + createConditions.forEach((severity, condition) -> { + createRules.put(severity, toAlarmRule(condition)); + }); + AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; + + AlarmRuleDefinition alarmRuleDefinition = new AlarmRuleDefinition(); + alarmRuleDefinition.setEntityId(entityId); + alarmRuleDefinition.setName(alarmType); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + configuration.setArguments(arguments); + configuration.setCreateRules(createRules); + configuration.setClearRule(clearRule); + alarmRuleDefinition.setConfiguration(configuration); + alarmRuleDefinition.setDebugSettings(DebugSettings.all()); + if (modifier.length > 0) { + modifier[0].accept(configuration); + } + AlarmRuleDefinition saved = saveAlarmRule(alarmRuleDefinition); + + CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> getDebugEvents(saved.getId(), 1), + events -> !events.isEmpty()).get(0); + latestEventId = debugEvent.getId(); + return saved; + } +``` + +- [ ] **Step 4: Update all test methods that call `createAlarmCf`** + +Search and replace all occurrences of `createAlarmCf` with `createAlarmRule` in AlarmRulesTest.java. Also update the local variable type from `CalculatedField` to `AlarmRuleDefinition` wherever the return value of `createAlarmRule` is stored. For example, change: + +```java +CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", arguments, createRules, clearRule); +``` + +to: + +```java +AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, clearRule); +``` + +Then update references from `calculatedField` to `alarmRule` in the assertion calls like `checkAlarmResult(calculatedField, ...)` → `checkAlarmResult(alarmRule, ...)`. + +- [ ] **Step 5: Update `checkAlarmResult` to accept AlarmRuleDefinition** + +The `checkAlarmResult` method currently takes `CalculatedField`. Update it to accept `AlarmRuleDefinition`: + +```java + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer assertion) { + return checkAlarmResult(alarmRule, assertion, null); + } + + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer assertion, Predicate waitFor) { + TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> getLatestAlarmResult(alarmRule.getId()), + result -> result != null && (waitFor == null || waitFor.test(result))); + assertion.accept(alarmResult); + + Alarm alarm = alarmResult.getAlarm(); + assertThat(alarm.getOriginator()).isEqualTo(originatorId); + assertThat(alarm.getType()).isEqualTo(alarmRule.getName()); + return alarmResult; + } +``` + +- [ ] **Step 6: Remove unused `CalculatedField` and `CalculatedFieldType` imports if no longer referenced** + +After all changes, remove the import for `CalculatedFieldType` and `CalculatedField` from AlarmRulesTest if they are no longer used: + +```java +// Remove if unused: +// import org.thingsboard.server.common.data.cf.CalculatedField; +// import org.thingsboard.server.common.data.cf.CalculatedFieldType; +``` + +Keep `CalculatedFieldId` import since it's still used by `getDebugEvents`. + +- [ ] **Step 7: Verify test compilation** + +Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn test-compile -pl application -am -q -DskipTests 2>&1 | tail -10` +Expected: BUILD SUCCESS + +- [ ] **Step 8: Run AlarmRulesTest** + +Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn test -pl application -Dtest="org.thingsboard.server.cf.AlarmRulesTest" -DfailIfNoTests=false 2>&1 | tail -30` +Expected: All tests pass. If any test fails, investigate and fix the variable name/type mismatches. + +- [ ] **Step 9: Commit** + +```bash +git add application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +git add application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +git commit -m "Update AlarmRulesTest to use new alarm rule API" +``` + +--- + +### Task 5: Create UI AlarmRulesService + +**Files:** +- Create: `ui-ngx/src/app/core/http/alarm-rules.service.ts` + +- [ ] **Step 1: Create the service** + +```typescript +/// +/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// +/// Copyright © 2016-2026 ThingsBoard, Inc. All Rights Reserved. +/// +/// NOTICE: All information contained herein is, and remains +/// the property of ThingsBoard, Inc. and its suppliers, +/// if any. The intellectual and technical concepts contained +/// herein are proprietary to ThingsBoard, Inc. +/// and its suppliers and may be covered by U.S. and Foreign Patents, +/// patents in process, and are protected by trade secret or copyright law. +/// +/// Dissemination of this information or reproduction of this material is strictly forbidden +/// unless prior written permission is obtained from COMPANY. +/// +/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, +/// managers or contractors who have executed Confidentiality and Non-disclosure agreements +/// explicitly covering such access. +/// +/// The copyright notice above does not evidence any actual or intended publication +/// or disclosure of this source code, which includes +/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. +/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, +/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT +/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, +/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. +/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION +/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, +/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// + +import { Injectable } from '@angular/core'; +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, +} from '@shared/models/calculated-field.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityTestScriptResult } from '@shared/models/entity.models'; +import { CalculatedFieldEventBody } from '@shared/models/event.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AlarmRulesService { + + constructor( + private http: HttpClient + ) { } + + public getAlarmRuleById(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAlarmRule(alarmRule: CalculatedField, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarmRule(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + } + + public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rule/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule/testScript', inputParams, defaultHttpOptionsFromConfig(config)); + } + + public getLatestAlarmRuleDebugEvent(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${id}/debug`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRuleNames(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules/names${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add ui-ngx/src/app/core/http/alarm-rules.service.ts +git commit -m "Add AlarmRulesService for UI" +``` + +--- + +### Task 6: Update UI components to use AlarmRulesService + +**Files:** +- Modify: `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts` +- Modify: `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts` +- Modify: `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts` + +- [ ] **Step 1: Update alarm-rules-table-config.ts** + +In `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts`: + +1. Add import for `AlarmRulesService`: +```typescript +import { AlarmRulesService } from '@core/http/alarm-rules.service'; +``` + +2. Change the constructor parameter type from `CalculatedFieldsService` to `AlarmRulesService` (line 105): +```typescript +constructor(private alarmRulesService: AlarmRulesService, +``` + +3. Update all service call sites. Replace: + - `this.calculatedFieldsService.getCalculatedFieldById(id.id)` → `this.alarmRulesService.getAlarmRuleById(id.id)` (line 155) + - `this.calculatedFieldsService.saveCalculatedField(alarmRule)` → `this.alarmRulesService.saveAlarmRule(alarmRule)` (line 156) + - `this.calculatedFieldsService.deleteCalculatedField(id.id)` → `this.alarmRulesService.deleteAlarmRule(id.id)` (line 165) + - `this.calculatedFieldsService.saveCalculatedField(calculatedField)` → `this.alarmRulesService.saveAlarmRule(calculatedField)` (line 393, in `importCalculatedField`) + +4. Update `fetchCalculatedFields` method (line 248-252): +```typescript + fetchCalculatedFields(pageLink: PageLink): Observable> { + return this.pageMode ? + this.alarmRulesService.getAlarmRules(pageLink, this.alarmRuleFilterConfig) : + this.alarmRulesService.getAlarmRulesByEntityId(this.entityId, pageLink); + } +``` + +Note: The `getAlarmRules` call no longer passes `types: [CalculatedFieldType.ALARM]` since the endpoint hardcodes that. The `alarmRuleFilterConfig` query object may still contain entity type filters which will be passed as query params. + +5. Update `onDebugConfigChanged` method (line 414-419): +```typescript + private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { + this.alarmRulesService.getAlarmRuleById(id).pipe( + switchMap(field => this.alarmRulesService.saveAlarmRule({ ...field, debugSettings })), + catchError(() => of(null)), + takeUntilDestroyed(this.destroyRef), + ).subscribe(() => this.updateData()); + } +``` + +6. Remove the `CalculatedFieldsService` import if it's no longer needed. Keep `CalculatedFieldType` import since it's still referenced in `importCalculatedField` for type validation check. + +- [ ] **Step 2: Update alarm-rules-table.component.ts** + +In `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts`: + +1. Replace `CalculatedFieldsService` import with `AlarmRulesService`: +```typescript +import { AlarmRulesService } from '@core/http/alarm-rules.service'; +``` + +2. Change the injected service in the constructor from `CalculatedFieldsService` to `AlarmRulesService`: +```typescript +constructor(private alarmRulesService: AlarmRulesService, +``` + +3. Update the `AlarmRulesTableConfig` instantiation to pass `alarmRulesService` instead of `calculatedFieldsService`: +```typescript +this.alarmRulesTableConfig = new AlarmRulesTableConfig( + this.alarmRulesService, + // ... remaining parameters stay the same +); +``` + +- [ ] **Step 3: Update alarm-rule-dialog.component.ts** + +In `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts`: + +1. Add import for `AlarmRulesService`: +```typescript +import { AlarmRulesService } from '@core/http/alarm-rules.service'; +``` + +2. Change the constructor injection from `CalculatedFieldsService` to `AlarmRulesService`: +```typescript +private alarmRulesService: AlarmRulesService, +``` + +3. Update the `add()` method (around line 206) to use the new service: +```typescript +this.alarmRulesService.saveAlarmRule(alarmRule) +``` + +4. Remove the `CalculatedFieldsService` import if no longer used. + +- [ ] **Step 4: Verify UI build** + +Run: `cd /Users/viacheslav/Desktop/thingsboard-pe/ui-ngx && npx ng build --configuration production 2>&1 | tail -20` + +If the build is slow, alternatively verify just type checking: +Run: `cd /Users/viacheslav/Desktop/thingsboard-pe/ui-ngx && npx tsc --noEmit 2>&1 | head -30` + +Expected: No compilation errors. + +- [ ] **Step 5: Commit** + +```bash +git add ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +git add ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +git add ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +git commit -m "Switch UI alarm rule components to AlarmRulesService" +``` diff --git a/docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md b/docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md new file mode 100644 index 0000000000..3c99e84fb5 --- /dev/null +++ b/docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md @@ -0,0 +1,144 @@ +# Alarm Rule Controller Design + +## Problem + +Alarm rules in ThingsBoard PE are internally implemented as calculated fields with `CalculatedFieldType.ALARM`. API consumers must create alarm rules via the `CalculatedFieldController`, setting `type = ALARM` — exposing an implementation detail that is confusing and leaks the CF abstraction. + +## Goal + +Create a dedicated `AlarmRuleController` that provides a clean, alarm-focused REST API while delegating to the existing `TbCalculatedFieldService` underneath. Update the UI and tests to use the new endpoints. + +## Approach + +Thin controller wrapper: `AlarmRuleController` delegates directly to `TbCalculatedFieldService`. A new `AlarmRuleDefinition` DTO wraps `CalculatedField` with the `type` field hidden (always ALARM). Conversion happens inline in the controller. No new service layer. + +The existing `CalculatedFieldController` remains unchanged. + +## DTOs + +### AlarmRuleDefinition + +Located in `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java`. + +Same structure as `CalculatedField` but without the `type` field: + +- `id`: `CalculatedFieldId` (reused, not a new ID type) +- `tenantId`: `TenantId` +- `name`: `String` +- `entityId`: `EntityId` +- `configuration`: `AlarmCalculatedFieldConfiguration` +- `createdTime`: `long` +- `configurationVersion`: `int` + +Provides conversion methods: +- `toCalculatedField()` — sets `type = ALARM`, copies all fields +- `static fromCalculatedField(CalculatedField cf)` — strips `type`, copies all fields + +### AlarmRuleDefinitionInfo + +Extends `AlarmRuleDefinition`, adds `entityName: String`. Mirrors the `CalculatedFieldInfo` pattern. + +Provides: +- `static fromCalculatedFieldInfo(CalculatedFieldInfo cfi)` — converts from the existing info type + +## Controller + +### AlarmRuleController + +Located in `application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java`. + +- `@RequestMapping("/api/alarm/rule")` for single-entity operations +- Extends `BaseController` +- Injects `TbCalculatedFieldService`, `EventService`, `TbelInvokeService` +- All endpoints require `TENANT_ADMIN` authority +- Uses existing `Operation.WRITE_CALCULATED_FIELD` / `READ_CALCULATED_FIELD` permissions + +### Endpoints + +| Method | Path | Description | Delegates To | +|--------|------|-------------|--------------| +| `POST` | `/api/alarm/rule` | Create or update alarm rule | `TbCalculatedFieldService.save()` | +| `GET` | `/api/alarm/rule/{alarmRuleId}` | Get alarm rule by ID | `TbCalculatedFieldService.findById()` | +| `GET` | `/api/alarm/rule/{entityType}/{entityId}` | Get alarm rules by entity (paged) | `TbCalculatedFieldService.findByTenantIdAndEntityId()` with `type=ALARM` | +| `GET` | `/api/alarm/rules` | List all alarm rules (paged, filtered) | `calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter()` with `types={ALARM}` | +| `GET` | `/api/alarm/rules/names` | Get alarm rule names (paged) | `calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType()` with `type=ALARM` | +| `DELETE` | `/api/alarm/rule/{alarmRuleId}` | Delete alarm rule | `TbCalculatedFieldService.delete()` | +| `GET` | `/api/alarm/rule/{alarmRuleId}/debug` | Get latest debug event | `EventService.findLatestEvents()` | +| `POST` | `/api/alarm/rule/testScript` | Test TBEL expression | Same logic as CF testScript | + +### Save endpoint details + +Accepts `AlarmRuleDefinition`. The controller: +1. Converts to `CalculatedField` via `toCalculatedField()` (sets `type = ALARM`) +2. Sets `tenantId` from the current user +3. Checks entity permissions (`Operation.WRITE_CALCULATED_FIELD`) +4. Checks referenced entities in the configuration +5. Calls `TbCalculatedFieldService.save()` +6. Converts result back to `AlarmRuleDefinition` + +### List endpoint details + +Accepts the same filter parameters as the CF list endpoint (entity type, entity IDs, text search, paging) but: +- Does NOT accept `types` parameter (hardcoded to `{ALARM}`) +- Does NOT accept `name` parameter (from the CF endpoint's multi-value `name` query param) +- Returns `PageData` (converted from `PageData`) +- Entity type filter defaults to all entity types that support ALARM (DEVICE, ASSET, CUSTOMER, DEVICE_PROFILE, ASSET_PROFILE), with the same permission check pattern as the CF controller + +### Get by entity endpoint details + +Accepts `entityType`, `entityId`, paging parameters. Does NOT accept `type` parameter (hardcoded to `ALARM`). Returns `PageData`. + +## UI Changes + +### New service: alarm-rules.service.ts + +Located in `ui-ngx/src/app/core/http/alarm-rules.service.ts`. + +Follows the same `@Injectable({ providedIn: 'root' })` pattern as `CalculatedFieldsService`. Methods: + +- `saveAlarmRule(rule: AlarmRuleDefinition)` -> `POST /api/alarm/rule` +- `getAlarmRuleById(id: string)` -> `GET /api/alarm/rule/{id}` +- `getAlarmRulesByEntityId(entityId: EntityId, pageLink: PageLink)` -> `GET /api/alarm/rule/{entityType}/{entityId}` +- `getAlarmRules(pageLink: PageLink, query)` -> `GET /api/alarm/rules` +- `getAlarmRuleNames(pageLink: PageLink)` -> `GET /api/alarm/rules/names` +- `deleteAlarmRule(id: string)` -> `DELETE /api/alarm/rule/{id}` +- `getLatestAlarmRuleDebugEvent(id: string)` -> `GET /api/alarm/rule/{id}/debug` +- `testScript(inputParams)` -> `POST /api/alarm/rule/testScript` + +### Consumer updates + +All components that currently call `CalculatedFieldsService` for ALARM-type operations switch to `AlarmRulesService`. Primary consumers: + +- `AlarmRuleDialogComponent` — switches `saveCalculatedField()` to `saveAlarmRule()` +- Any component loading alarm rules by entity — switches to `getAlarmRulesByEntityId()` +- Components listing alarm rules — switches to `getAlarmRules()` + +The TypeScript model type `CalculatedFieldAlarmRule` can be updated or aliased to match the new `AlarmRuleDefinition` shape (without the `type` discriminator field in the request). + +## Test Changes + +### AlarmRulesTest.java + +Located at `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java`. + +Updates: +- `saveCalculatedField()` helper changes from `POST /api/calculatedField` to `POST /api/alarm/rule` +- The helper builds `AlarmRuleDefinition` instead of `CalculatedField` (no `type` field, alarm configuration directly on the object) +- GET calls for alarm rules by entity change to `/api/alarm/rule/{entityType}/{entityId}` +- Debug event retrieval may optionally use `/api/alarm/rule/{id}/debug` instead of direct `EventDao` access (though the test currently uses `EventDao` directly for most checks, which is fine) +- The `createAlarmCf()` helper is renamed to `createAlarmRule()` and returns `AlarmRuleDefinition` + +## Scope boundaries + +**In scope:** +- New `AlarmRuleDefinition` and `AlarmRuleDefinitionInfo` DTOs +- New `AlarmRuleController` with all listed endpoints +- New `AlarmRulesService` on the UI side +- Update `AlarmRulesTest` to use new API +- Update UI components to use new service + +**Out of scope:** +- Changes to `CalculatedFieldController` (left as-is) +- New permission types (reuses `READ_CALCULATED_FIELD` / `WRITE_CALCULATED_FIELD`) +- Reprocessing endpoints (not applicable to alarm rules) +- New `TbAlarmRuleService` layer (thin wrapper approach, no new service) diff --git a/ui-ngx/src/app/core/http/alarm-rules.service.ts b/ui-ngx/src/app/core/http/alarm-rules.service.ts new file mode 100644 index 0000000000..1800b2a475 --- /dev/null +++ b/ui-ngx/src/app/core/http/alarm-rules.service.ts @@ -0,0 +1,73 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +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 +} from '@shared/models/calculated-field.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityTestScriptResult } from '@shared/models/entity.models'; +import { CalculatedFieldEventBody } from '@shared/models/event.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AlarmRulesService { + + constructor( + private http: HttpClient + ) { } + + public getAlarmRuleById(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAlarmRule(alarmRule: CalculatedField, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarmRule(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + } + + public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rule/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule/testScript', inputParams, defaultHttpOptionsFromConfig(config)); + } + + public getLatestAlarmRuleDebugEvent(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${id}/debug`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRuleNames(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules/names${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } +} 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 62cee00513..97cdd91689 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 @@ -25,7 +25,7 @@ import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@ import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; -import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { AlarmRulesService } from '@core/http/alarm-rules.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"; @@ -97,7 +97,7 @@ export class AlarmRuleDialogComponent extends DialogComponent, - private calculatedFieldsService: CalculatedFieldsService, + private alarmRulesService: AlarmRulesService, private destroyRef: DestroyRef, private cfFormService: CalculatedFieldFormService) { super(store, router, dialogRef); @@ -173,7 +173,7 @@ export class AlarmRuleDialogComponent extends DialogComponent this.dialogRef.close(calculatedField), 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 039ee7d5fd..06b9213e02 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 @@ -35,7 +35,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; import { catchError, filter, first, switchMap, tap } from 'rxjs/operators'; import { ArgumentEntityType, @@ -84,7 +84,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.fetchCalculatedFields(pageLink); this.addEntity = this.getCalculatedAlarmDialog.bind(this); - this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id); - this.saveEntity = (alarmRule) => this.calculatedFieldsService.saveCalculatedField(alarmRule); + this.loadEntity = id => this.alarmRulesService.getAlarmRuleById(id.id); + this.saveEntity = (alarmRule) => this.alarmRulesService.saveAlarmRule(alarmRule); 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'); - this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); + this.deleteEntity = id => this.alarmRulesService.deleteAlarmRule(id.id); this.onEntityAction = action => this.onCFAction(action); @@ -216,8 +216,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig> { return this.pageMode ? - this.calculatedFieldsService.getCalculatedFields(pageLink, {types: [CalculatedFieldType.ALARM], ...this.alarmRuleFilterConfig}) : - this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink, CalculatedFieldType.ALARM); + this.alarmRulesService.getAlarmRules(pageLink, this.alarmRuleFilterConfig) : + this.alarmRulesService.getAlarmRulesByEntityId(this.entityId, pageLink); } onOpenDebugConfig($event: Event, calculatedField: AlarmRuleTableEntity): void { @@ -353,7 +353,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), filter(Boolean), - switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), + switchMap(calculatedField => this.alarmRulesService.saveAlarmRule(calculatedField)), filter(Boolean), takeUntilDestroyed(this.destroyRef) ) @@ -375,8 +375,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), + this.alarmRulesService.getAlarmRuleById(id).pipe( + switchMap(field => this.alarmRulesService.saveAlarmRule({ ...field, debugSettings })), catchError(() => of(null)), takeUntilDestroyed(this.destroyRef), ).subscribe(() => this.updateData()); diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index a13aa2fac7..ce0aecb598 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -30,7 +30,7 @@ import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; @@ -59,7 +59,7 @@ export class AlarmRulesTableComponent { pageMode: boolean = false; - constructor(private calculatedFieldsService: CalculatedFieldsService, + constructor(private alarmRulesService: AlarmRulesService, private translate: TranslateService, private dialog: MatDialog, private store: Store, @@ -77,7 +77,7 @@ export class AlarmRulesTableComponent { effect(() => { if (this.active() || this.pageMode) { this.alarmRulesTableConfig = new AlarmRulesTableConfig( - this.calculatedFieldsService, + this.alarmRulesService, this.translate, this.dialog, this.datePipe, 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 b0606b9c5b..75dd645b40 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 @@ -26,7 +26,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; -import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -40,7 +40,7 @@ import { AlarmRulesTableConfig } from '@home/components/alarm-rules/alarm-rules- export const AlarmRulesTableConfigResolver: ResolveFn = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot, - calculatedFieldsService = inject(CalculatedFieldsService), + alarmRulesService = inject(AlarmRulesService), translate = inject(TranslateService), dialog = inject(MatDialog), store = inject(Store), @@ -52,7 +52,7 @@ export const AlarmRulesTableConfigResolver: ResolveFn = router = inject(Router), ) => { return new AlarmRulesTableConfig( - calculatedFieldsService, + alarmRulesService, translate, dialog, datePipe, From 0ef8dd513d27371e3168564aff083d93898b5605 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 31 Mar 2026 12:23:52 +0300 Subject: [PATCH 207/313] Refactor alarm rule controller and fix swagger docs --- .../controller/AlarmRuleController.java | 78 +- .../controller/CalculatedFieldController.java | 14 +- .../common/data/cf/AlarmRuleDefinition.java | 5 +- .../plans/2026-03-30-alarm-rule-controller.md | 985 ------------------ ...2026-03-30-alarm-rule-controller-design.md | 144 --- 5 files changed, 26 insertions(+), 1200 deletions(-) delete mode 100644 docs/superpowers/plans/2026-03-30-alarm-rule-controller.md delete mode 100644 docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java index dd3b51fc5d..243f311b49 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java @@ -39,8 +39,6 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfCtx; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; -import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; -import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; @@ -50,7 +48,6 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -59,18 +56,17 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; 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.service.security.permission.Operation; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; -import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -79,6 +75,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.CalculatedFieldController.TIMEOUT; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; @@ -99,35 +96,20 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class AlarmRuleController extends BaseController { private final TbCalculatedFieldService tbCalculatedFieldService; + private final CalculatedFieldController calculatedFieldController; private final EventService eventService; private final TbelInvokeService tbelInvokeService; public static final String ALARM_RULE_ID = "alarmRuleId"; - public static final int TIMEOUT = 20; - private static final String TEST_SCRIPT_EXPRESSION = - "Execute the Script expression and return the result. The format of request: \n\n" + "Execute the alarm rule TBEL condition expression and return the result. " + + "Alarm rule expressions must return a boolean value. The format of request: \n\n" + MARKDOWN_CODE_BLOCK_START + "{\n" + - " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + + " \"expression\": \"return temperature > 50;\",\n" + " \"arguments\": {\n" + - " \"temperature\": {\n" + - " \"type\": \"TS_ROLLING\",\n" + - " \"timeWindow\": {\n" + - " \"startTs\": 1739775630002,\n" + - " \"endTs\": 65432211,\n" + - " \"limit\": 5\n" + - " },\n" + - " \"values\": [\n" + - " { \"ts\": 1739775639851, \"value\": 23 },\n" + - " { \"ts\": 1739775664561, \"value\": 43 },\n" + - " { \"ts\": 1739775713079, \"value\": 15 },\n" + - " { \"ts\": 1739775999522, \"value\": 34 },\n" + - " { \"ts\": 1739776228452, \"value\": 22 }\n" + - " ]\n" + - " },\n" + - " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + + " \"temperature\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 55 }\n" + " }\n" + "}" + MARKDOWN_CODE_BLOCK_END @@ -147,14 +129,13 @@ public class AlarmRuleController extends BaseController { alarmRuleDefinition.setTenantId(getTenantId()); checkEntityId(alarmRuleDefinition.getEntityId(), Operation.WRITE_CALCULATED_FIELD); CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField(); - checkReferencedEntities(calculatedField.getConfiguration()); + calculatedFieldController.checkReferencedEntities(calculatedField.getConfiguration()); CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser()); return AlarmRuleDefinition.fromCalculatedField(saved); } @ApiOperation(value = "Get Alarm Rule (getAlarmRuleById)", - notes = "Fetch the Alarm Rule object based on the provided Alarm Rule Id." - ) + notes = "Fetch the Alarm Rule object based on the provided Alarm Rule Id." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping("/alarm/rule/{alarmRuleId}") public AlarmRuleDefinition getAlarmRuleById(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { @@ -167,8 +148,7 @@ public class AlarmRuleController extends BaseController { } @ApiOperation(value = "Get Alarm Rules by Entity Id (getAlarmRulesByEntityId)", - notes = "Fetch the Alarm Rules based on the provided Entity Id." - ) + notes = "Fetch the Alarm Rules based on the provided Entity Id." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/alarm/rule/{entityType}/{entityId}") public PageData getAlarmRulesByEntityId( @@ -188,7 +168,7 @@ public class AlarmRuleController extends BaseController { } @ApiOperation(value = "Get alarm rules (getAlarmRules)", - notes = "Fetch tenant alarm rules based on the filter.") + notes = "Fetch tenant alarm rules based on the filter." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/alarm/rules") public PageData getAlarmRules(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -230,7 +210,7 @@ public class AlarmRuleController extends BaseController { } @ApiOperation(value = "Get alarm rule names (getAlarmRuleNames)", - notes = "Fetch the list of alarm rule names.") + notes = "Fetch the list of alarm rule names." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/alarm/rules/names") public PageData getAlarmRuleNames(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -274,12 +254,12 @@ public class AlarmRuleController extends BaseController { .orElse(null); } - @ApiOperation(value = "Test Script expression", + @ApiOperation(value = "Test alarm rule TBEL expression (testAlarmRuleScript)", notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/alarm/rule/testScript") public JsonNode testAlarmRuleScript( - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL expression.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL condition expression. The expression must return a boolean value.") @RequestBody JsonNode inputParams) { String expression = inputParams.get("expression").asText(); Map arguments = Objects.requireNonNullElse( @@ -308,7 +288,7 @@ public class AlarmRuleController extends BaseController { ); Object[] args = new Object[ctxAndArgNames.size()]; - args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); + args[0] = new TbelCfCtx(arguments, CalculatedFieldController.getLatestTimestamp(arguments)); for (int i = 1; i < ctxAndArgNames.size(); i++) { var arg = arguments.get(ctxAndArgNames.get(i)); if (arg instanceof TbelCfSingleValueArg svArg) { @@ -334,32 +314,4 @@ public class AlarmRuleController extends BaseController { .put("error", errorText); } - private long getLatestTimestamp(Map arguments) { - long lastUpdateTimestamp = -1; - for (TbelCfArg entry : arguments.values()) { - if (entry instanceof TbelCfSingleValueArg singleValueArg) { - long ts = singleValueArg.getTs(); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); - } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { - long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); - } - } - return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; - } - - private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { - Set referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); - for (EntityId referencedEntityId : referencedEntityIds) { - EntityType entityTypeVal = referencedEntityId.getEntityType(); - switch (entityTypeVal) { - case TENANT -> { - return; - } - case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityTypeVal + "' for referenced entities."); - } - } - } - } diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index 14a9728c6b..cd69c717e8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -107,9 +107,9 @@ public class CalculatedFieldController extends BaseController { public static final String CALCULATED_FIELD_ID = "calculatedFieldId"; - public static final int TIMEOUT = 20; + static final int TIMEOUT = 20; - private static final String TEST_SCRIPT_EXPRESSION = + static final String TEST_SCRIPT_EXPRESSION = "Execute the Script expression and return the result. The format of request: \n\n" + MARKDOWN_CODE_BLOCK_START + "{\n" + @@ -359,7 +359,7 @@ public class CalculatedFieldController extends BaseController { .put("error", errorText); } - private long getLatestTimestamp(Map arguments) { + static long getLatestTimestamp(Map arguments) { long lastUpdateTimestamp = -1; for (TbelCfArg entry : arguments.values()) { if (entry instanceof TbelCfSingleValueArg singleValueArg) { @@ -373,16 +373,16 @@ public class CalculatedFieldController extends BaseController { return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; } - private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { + void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { Set referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); for (EntityId referencedEntityId : referencedEntityIds) { - EntityType entityType = referencedEntityId.getEntityType(); - switch (entityType) { + EntityType refEntityType = referencedEntityId.getEntityType(); + switch (refEntityType) { case TENANT -> { return; } case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); + default -> throw new IllegalArgumentException("Unsupported referenced entity type: '" + refEntityType + "'."); } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java index 67b743b3bb..6e608cd255 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java @@ -63,7 +63,10 @@ public class AlarmRuleDefinition extends BaseData implements private AlarmCalculatedFieldConfiguration configuration; private Long version; @NoXss - @Schema(description = "Additional parameters of the alarm rule") + @Schema(description = "Additional parameters of the alarm rule. " + + "May include: 'description' (string).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"High temperature alarm rule\"}") private JsonNode additionalInfo; public AlarmRuleDefinition() {} diff --git a/docs/superpowers/plans/2026-03-30-alarm-rule-controller.md b/docs/superpowers/plans/2026-03-30-alarm-rule-controller.md deleted file mode 100644 index af113ceaf1..0000000000 --- a/docs/superpowers/plans/2026-03-30-alarm-rule-controller.md +++ /dev/null @@ -1,985 +0,0 @@ -# Alarm Rule Controller Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Create a dedicated AlarmRuleController REST API that wraps the CalculatedField service layer, hiding the CF implementation detail from alarm rule API consumers. - -**Architecture:** Thin controller wrapper. New `AlarmRuleDefinition` and `AlarmRuleDefinitionInfo` DTOs wrap `CalculatedField`/`CalculatedFieldInfo`. `AlarmRuleController` delegates to `TbCalculatedFieldService` and `CalculatedFieldService` (DAO). UI gets a new `AlarmRulesService` pointing at the new endpoints. `AlarmRulesTest` switches to the new API. - -**Tech Stack:** Java 17+, Spring Boot, Angular 17+, JUnit 4 / Spring MockMvc - ---- - -## File Structure - -**Backend (new):** -- `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java` — DTO wrapping CalculatedField without `type` -- `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java` — extends AlarmRuleDefinition, adds `entityName` -- `application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java` — REST controller - -**Backend (modify):** -- `application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java:1652` — add `saveAlarmRule()` helper alongside existing `saveCalculatedField()` -- `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java` — switch from CF API to alarm rule API - -**Frontend (new):** -- `ui-ngx/src/app/core/http/alarm-rules.service.ts` — Angular HTTP service for alarm rule endpoints - -**Frontend (modify):** -- `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts` — switch from `CalculatedFieldsService` to `AlarmRulesService` -- `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts` — inject `AlarmRulesService` instead of `CalculatedFieldsService` -- `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts` — use `AlarmRulesService` for all CRUD operations - ---- - -### Task 1: Create AlarmRuleDefinition DTO - -**Files:** -- Create: `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java` - -- [ ] **Step 1: Create AlarmRuleDefinition class** - -```java -package org.thingsboard.server.common.data.cf; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.databind.JsonNode; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.HasAdditionalInfo; -import org.thingsboard.server.common.data.HasDebugSettings; -import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.HasVersion; -import org.thingsboard.server.common.data.TenantEntity; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.debug.DebugSettings; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.validation.Length; -import org.thingsboard.server.common.data.validation.NoXss; - -@Schema -@Data -@EqualsAndHashCode(callSuper = true) -public class AlarmRuleDefinition extends BaseData implements HasName, TenantEntity, HasVersion, HasDebugSettings, HasAdditionalInfo { - - private TenantId tenantId; - private EntityId entityId; - - @NoXss - @Length(fieldName = "name") - @Schema(description = "Alarm type name.") - private String name; - @Deprecated - @Schema(description = "Enable/disable debug.", example = "false", deprecated = true) - private boolean debugMode; - @Schema(description = "Debug settings object.") - private DebugSettings debugSettings; - @Schema(description = "Version of alarm rule configuration.", example = "0") - private int configurationVersion; - @Schema(implementation = AlarmCalculatedFieldConfiguration.class) - @Valid - @NotNull - private AlarmCalculatedFieldConfiguration configuration; - private Long version; - @NoXss - @Schema(description = "Additional parameters of the alarm rule") - private JsonNode additionalInfo; - - public AlarmRuleDefinition() {} - - public AlarmRuleDefinition(CalculatedFieldId id) { - super(id); - } - - public AlarmRuleDefinition(AlarmRuleDefinition other) { - super(other); - this.tenantId = other.tenantId; - this.entityId = other.entityId; - this.name = other.name; - this.debugMode = other.debugMode; - this.debugSettings = other.debugSettings; - this.configurationVersion = other.configurationVersion; - this.configuration = other.configuration; - this.version = other.version; - this.additionalInfo = other.additionalInfo; - } - - @Schema(description = "JSON object with the Alarm Rule Id.") - @Override - public CalculatedFieldId getId() { - return super.getId(); - } - - @Schema(description = "Timestamp of the alarm rule creation, in milliseconds", example = "1609459200000", accessMode = Schema.AccessMode.READ_ONLY) - @Override - public long getCreatedTime() { - return super.getCreatedTime(); - } - - @JsonIgnore - public boolean isDebugMode() { - return debugMode; - } - - @JsonSetter - public void setDebugMode(boolean debugMode) { - this.debugMode = debugMode; - } - - @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD; - } - - public CalculatedField toCalculatedField() { - CalculatedField cf = new CalculatedField(); - cf.setId(this.getId()); - cf.setCreatedTime(this.getCreatedTime()); - cf.setTenantId(this.tenantId); - cf.setEntityId(this.entityId); - cf.setType(CalculatedFieldType.ALARM); - cf.setName(this.name); - cf.setDebugMode(this.debugMode); - cf.setDebugSettings(this.debugSettings); - cf.setConfigurationVersion(this.configurationVersion); - cf.setConfiguration(this.configuration); - cf.setVersion(this.version); - cf.setAdditionalInfo(this.additionalInfo); - return cf; - } - - public static AlarmRuleDefinition fromCalculatedField(CalculatedField cf) { - AlarmRuleDefinition def = new AlarmRuleDefinition(); - def.setId(cf.getId()); - def.setCreatedTime(cf.getCreatedTime()); - def.setTenantId(cf.getTenantId()); - def.setEntityId(cf.getEntityId()); - def.setName(cf.getName()); - def.setDebugMode(cf.isDebugMode()); - def.setDebugSettings(cf.getDebugSettings()); - def.setConfigurationVersion(cf.getConfigurationVersion()); - def.setConfiguration((AlarmCalculatedFieldConfiguration) cf.getConfiguration()); - def.setVersion(cf.getVersion()); - def.setAdditionalInfo(cf.getAdditionalInfo()); - return def; - } -} -``` - -- [ ] **Step 2: Verify compilation** - -Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn compile -pl common/data -am -q -DskipTests 2>&1 | tail -5` -Expected: BUILD SUCCESS - -- [ ] **Step 3: Commit** - -```bash -git add common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java -git commit -m "Add AlarmRuleDefinition DTO" -``` - ---- - -### Task 2: Create AlarmRuleDefinitionInfo DTO - -**Files:** -- Create: `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java` - -- [ ] **Step 1: Create AlarmRuleDefinitionInfo class** - -```java -package org.thingsboard.server.common.data.cf; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -public class AlarmRuleDefinitionInfo extends AlarmRuleDefinition { - - private String entityName; - - public AlarmRuleDefinitionInfo(AlarmRuleDefinition alarmRuleDefinition, String entityName) { - super(alarmRuleDefinition); - this.entityName = entityName; - } - - public static AlarmRuleDefinitionInfo fromCalculatedFieldInfo(CalculatedFieldInfo cfi) { - AlarmRuleDefinitionInfo info = new AlarmRuleDefinitionInfo(); - AlarmRuleDefinition def = AlarmRuleDefinition.fromCalculatedField(cfi); - info.setId(def.getId()); - info.setCreatedTime(def.getCreatedTime()); - info.setTenantId(def.getTenantId()); - info.setEntityId(def.getEntityId()); - info.setName(def.getName()); - info.setDebugMode(def.isDebugMode()); - info.setDebugSettings(def.getDebugSettings()); - info.setConfigurationVersion(def.getConfigurationVersion()); - info.setConfiguration(def.getConfiguration()); - info.setVersion(def.getVersion()); - info.setAdditionalInfo(def.getAdditionalInfo()); - info.setEntityName(cfi.getEntityName()); - return info; - } -} -``` - -- [ ] **Step 2: Verify compilation** - -Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn compile -pl common/data -am -q -DskipTests 2>&1 | tail -5` -Expected: BUILD SUCCESS - -- [ ] **Step 3: Commit** - -```bash -git add common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java -git commit -m "Add AlarmRuleDefinitionInfo DTO" -``` - ---- - -### Task 3: Create AlarmRuleController - -**Files:** -- Create: `application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java` - -- [ ] **Step 1: Create the controller with all endpoints** - -```java -package org.thingsboard.server.controller; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.script.api.tbel.TbelCfCtx; -import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; -import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; -import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; -import org.thingsboard.script.api.tbel.TbelInvokeService; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EventInfo; -import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; -import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; -import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; -import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.event.EventType; -import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -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.permission.Operation; -import org.thingsboard.server.common.data.permission.Resource; -import org.thingsboard.server.config.annotations.ApiOperation; -import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; -import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; -import org.thingsboard.server.service.security.model.SecurityUser; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_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.PAGE_NUMBER_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; -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.TENANT_AUTHORITY_PARAGRAPH; -import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; - -@RestController -@TbCoreComponent -@RequestMapping("/api") -@RequiredArgsConstructor -@Slf4j -public class AlarmRuleController extends BaseController { - - private final TbCalculatedFieldService tbCalculatedFieldService; - private final EventService eventService; - private final TbelInvokeService tbelInvokeService; - - private static final String ALARM_RULE_ID = "alarmRuleId"; - private static final int TIMEOUT = 20; - - private static final String TEST_SCRIPT_EXPRESSION = - "Execute the Script expression and return the result. The format of request: \n\n" - + MARKDOWN_CODE_BLOCK_START - + "{\n" + - " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + - " \"arguments\": {\n" + - " \"temperature\": {\n" + - " \"type\": \"TS_ROLLING\",\n" + - " \"timeWindow\": {\n" + - " \"startTs\": 1739775630002,\n" + - " \"endTs\": 65432211,\n" + - " \"limit\": 5\n" + - " },\n" + - " \"values\": [\n" + - " { \"ts\": 1739775639851, \"value\": 23 },\n" + - " { \"ts\": 1739775664561, \"value\": 43 },\n" + - " { \"ts\": 1739775713079, \"value\": 15 },\n" + - " { \"ts\": 1739775999522, \"value\": 34 },\n" + - " { \"ts\": 1739776228452, \"value\": 22 }\n" + - " ]\n" + - " },\n" + - " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + - " }\n" + - "}" - + MARKDOWN_CODE_BLOCK_END - + "\n\n Expected result JSON contains \"output\" and \"error\"."; - - @ApiOperation(value = "Create Or Update Alarm Rule (saveAlarmRule)", - notes = "Creates or Updates the Alarm Rule. When creating, platform generates Alarm Rule Id as " + UUID_WIKI_LINK + - "The newly created Alarm Rule Id will be present in the response. " + - "Specify existing Alarm Rule Id to update. " + - "Referencing non-existing Alarm Rule Id will cause 'Not Found' error. " + - "Remove 'id', 'tenantId' from the request body example (below) to create new Alarm Rule entity. " - + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @PostMapping("/alarm/rule") - public AlarmRuleDefinition saveAlarmRule( - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm rule.") - @RequestBody AlarmRuleDefinition alarmRuleDefinition) throws Exception { - alarmRuleDefinition.setTenantId(getTenantId()); - CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField(); - checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); - checkReferencedEntities(calculatedField); - CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser()); - return AlarmRuleDefinition.fromCalculatedField(saved); - } - - @ApiOperation(value = "Get Alarm Rule (getAlarmRuleById)", - notes = "Fetch the Alarm Rule object based on the provided Alarm Rule Id." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/alarm/rule/{alarmRuleId}") - public AlarmRuleDefinition getAlarmRuleById( - @Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { - checkParameter(ALARM_RULE_ID, strAlarmRuleId); - CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); - CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); - checkNotNull(calculatedField); - checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); - return AlarmRuleDefinition.fromCalculatedField(calculatedField); - } - - @ApiOperation(value = "Get Alarm Rules by Entity Id (getAlarmRulesByEntityId)", - notes = "Fetch the Alarm Rules based on the provided Entity Id." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/alarm/rule/{entityType}/{entityId}") - public PageData getAlarmRulesByEntityId( - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, - @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = "Filter by alarm rule name.") @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - checkParameter("entityId", entityIdStr); - EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); - checkEntityId(entityId, Operation.READ_CALCULATED_FIELD); - PageData result = checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, CalculatedFieldType.ALARM, pageLink)); - return result.mapData(AlarmRuleDefinition::fromCalculatedField); - } - - @ApiOperation(value = "Get alarm rules (getAlarmRules)", - notes = "Fetch tenant alarm rules based on the filter." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/alarm/rules") - public PageData getAlarmRules( - @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = "Entity type filter.") @RequestParam(required = false) EntityType entityType, - @Parameter(description = "Entities filter.") @RequestParam(required = false) Set entities, - @Parameter(description = "Filter by alarm rule name.") @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - SecurityUser user = getCurrentUser(); - - Set types = EnumSet.of(CalculatedFieldType.ALARM); - - Set entityTypes; - if (entityType == null) { - entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream() - .filter(entry -> entry.getValue().contains(CalculatedFieldType.ALARM)) - .map(Map.Entry::getKey) - .filter(t -> { - try { - return accessControlService.hasPermission(user, Resource.resourceFromEntityType(t), Operation.READ_CALCULATED_FIELD); - } catch (ThingsboardException e) { - return false; - } - }) - .collect(Collectors.toSet()); - } else { - accessControlService.checkPermission(user, Resource.resourceFromEntityType(entityType), Operation.READ_CALCULATED_FIELD); - entityTypes = EnumSet.of(entityType); - } - - CalculatedFieldFilter filter = CalculatedFieldFilter.builder() - .types(types) - .entityTypes(entityTypes) - .entityIds(entities) - .build(); - PageData result = calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink); - return result.mapData(AlarmRuleDefinitionInfo::fromCalculatedFieldInfo); - } - - @ApiOperation(value = "Get alarm rule names (getAlarmRuleNames)", - notes = "Fetch the list of alarm rule names." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/alarm/rules/names") - public PageData getAlarmRuleNames( - @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = "Filter by alarm rule name.") @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - PageLink pageLink = createPageLink(pageSize, page, textSearch, "name", sortOrder); - return calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType(getTenantId(), CalculatedFieldType.ALARM, pageLink); - } - - @ApiOperation(value = "Delete Alarm Rule (deleteAlarmRule)", - notes = "Deletes the alarm rule. Referencing non-existing Alarm Rule Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @DeleteMapping("/alarm/rule/{alarmRuleId}") - @ResponseStatus(HttpStatus.OK) - public void deleteAlarmRule(@PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws Exception { - checkParameter(ALARM_RULE_ID, strAlarmRuleId); - CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); - CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); - checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); - tbCalculatedFieldService.delete(calculatedField, getCurrentUser()); - } - - @ApiOperation(value = "Get latest alarm rule debug event (getLatestAlarmRuleDebugEvent)", - notes = "Gets latest alarm rule debug event for specified alarm rule id. " + - "Referencing non-existing alarm rule id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/alarm/rule/{alarmRuleId}/debug") - public JsonNode getLatestAlarmRuleDebugEvent( - @Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { - checkParameter(ALARM_RULE_ID, strAlarmRuleId); - CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); - CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); - checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); - TenantId tenantId = getCurrentUser().getTenantId(); - return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1)) - .flatMap(events -> events.stream().map(EventInfo::getBody).findFirst()) - .orElse(null); - } - - @ApiOperation(value = "Test Script expression", - notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @PostMapping("/alarm/rule/testScript") - public JsonNode testAlarmRuleScript( - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL expression.") - @RequestBody JsonNode inputParams) { - String expression = inputParams.get("expression").asText(); - Map arguments = Objects.requireNonNullElse( - JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), - Collections.emptyMap() - ); - - ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); - ctxAndArgNames.add("ctx"); - ctxAndArgNames.addAll(arguments.keySet()); - - String output = ""; - String errorText = ""; - - CalculatedFieldTbelScriptEngine engine = null; - try { - if (tbelInvokeService == null) { - throw new IllegalArgumentException("TBEL script engine is disabled!"); - } - - engine = new CalculatedFieldTbelScriptEngine( - getTenantId(), - tbelInvokeService, - expression, - ctxAndArgNames.toArray(String[]::new) - ); - - Object[] args = new Object[ctxAndArgNames.size()]; - args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); - for (int i = 1; i < ctxAndArgNames.size(); i++) { - var arg = arguments.get(ctxAndArgNames.get(i)); - if (arg instanceof TbelCfSingleValueArg svArg) { - args[i] = svArg.getValue(); - } else { - args[i] = arg; - } - } - - JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); - output = JacksonUtil.toString(json); - } catch (Exception e) { - log.error("Error evaluating expression", e); - Throwable rootCause = ExceptionUtils.getRootCause(e); - errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); - } finally { - if (engine != null) { - engine.destroy(); - } - } - return JacksonUtil.newObjectNode() - .put("output", output) - .put("error", errorText); - } - - private long getLatestTimestamp(Map arguments) { - long lastUpdateTimestamp = -1; - for (TbelCfArg entry : arguments.values()) { - if (entry instanceof TbelCfSingleValueArg singleValueArg) { - long ts = singleValueArg.getTs(); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); - } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { - long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); - } - } - return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; - } - - private void checkReferencedEntities(CalculatedField calculatedField) throws ThingsboardException { - Set referencedEntityIds = calculatedField.getConfiguration().getReferencedEntities(); - for (EntityId referencedEntityId : referencedEntityIds) { - EntityType refEntityType = referencedEntityId.getEntityType(); - switch (refEntityType) { - case TENANT -> { - return; - } - case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> throw new IllegalArgumentException("Alarm rules do not support '" + refEntityType + "' for referenced entities."); - } - } - } -} -``` - -- [ ] **Step 2: Check that `PageData.mapData` exists** - -The controller uses `result.mapData(...)` to convert page results. Verify this method exists: - -Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && grep -n 'mapData' common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java` - -If `mapData` doesn't exist, replace with manual conversion: -```java -new PageData<>(result.getData().stream().map(AlarmRuleDefinition::fromCalculatedField).toList(), result.getTotalPages(), result.getTotalElements(), result.hasNext()) -``` - -- [ ] **Step 3: Verify compilation** - -Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn compile -pl application -am -q -DskipTests 2>&1 | tail -10` -Expected: BUILD SUCCESS - -- [ ] **Step 4: Commit** - -```bash -git add application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java -git commit -m "Add AlarmRuleController with all endpoints" -``` - ---- - -### Task 4: Update AlarmRulesTest to use new API - -**Files:** -- Modify: `application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java:1652` -- Modify: `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java` - -- [ ] **Step 1: Add `saveAlarmRule` helper to AbstractWebTest** - -In `application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java`, after the existing `saveCalculatedField` method at line 1652-1654, add: - -```java - protected AlarmRuleDefinition saveAlarmRule(AlarmRuleDefinition alarmRule) { - return doPost("/api/alarm/rule", alarmRule, AlarmRuleDefinition.class); - } -``` - -Also add the necessary import at the top of AbstractWebTest.java: -```java -import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; -``` - -- [ ] **Step 2: Update AlarmRulesTest imports** - -In `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java`, add the import: - -```java -import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; -``` - -- [ ] **Step 3: Update `createAlarmCf` to `createAlarmRule` using new DTO** - -Replace the `createAlarmCf` method (starting at line 955) with: - -```java - private AlarmRuleDefinition createAlarmRule(EntityId entityId, - String alarmType, - Map arguments, - Map createConditions, - Condition clearCondition, - Consumer... modifier) { - Map createRules = new HashMap<>(); - createConditions.forEach((severity, condition) -> { - createRules.put(severity, toAlarmRule(condition)); - }); - AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; - - AlarmRuleDefinition alarmRuleDefinition = new AlarmRuleDefinition(); - alarmRuleDefinition.setEntityId(entityId); - alarmRuleDefinition.setName(alarmType); - AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); - configuration.setArguments(arguments); - configuration.setCreateRules(createRules); - configuration.setClearRule(clearRule); - alarmRuleDefinition.setConfiguration(configuration); - alarmRuleDefinition.setDebugSettings(DebugSettings.all()); - if (modifier.length > 0) { - modifier[0].accept(configuration); - } - AlarmRuleDefinition saved = saveAlarmRule(alarmRuleDefinition); - - CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> getDebugEvents(saved.getId(), 1), - events -> !events.isEmpty()).get(0); - latestEventId = debugEvent.getId(); - return saved; - } -``` - -- [ ] **Step 4: Update all test methods that call `createAlarmCf`** - -Search and replace all occurrences of `createAlarmCf` with `createAlarmRule` in AlarmRulesTest.java. Also update the local variable type from `CalculatedField` to `AlarmRuleDefinition` wherever the return value of `createAlarmRule` is stored. For example, change: - -```java -CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", arguments, createRules, clearRule); -``` - -to: - -```java -AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, clearRule); -``` - -Then update references from `calculatedField` to `alarmRule` in the assertion calls like `checkAlarmResult(calculatedField, ...)` → `checkAlarmResult(alarmRule, ...)`. - -- [ ] **Step 5: Update `checkAlarmResult` to accept AlarmRuleDefinition** - -The `checkAlarmResult` method currently takes `CalculatedField`. Update it to accept `AlarmRuleDefinition`: - -```java - private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer assertion) { - return checkAlarmResult(alarmRule, assertion, null); - } - - private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer assertion, Predicate waitFor) { - TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> getLatestAlarmResult(alarmRule.getId()), - result -> result != null && (waitFor == null || waitFor.test(result))); - assertion.accept(alarmResult); - - Alarm alarm = alarmResult.getAlarm(); - assertThat(alarm.getOriginator()).isEqualTo(originatorId); - assertThat(alarm.getType()).isEqualTo(alarmRule.getName()); - return alarmResult; - } -``` - -- [ ] **Step 6: Remove unused `CalculatedField` and `CalculatedFieldType` imports if no longer referenced** - -After all changes, remove the import for `CalculatedFieldType` and `CalculatedField` from AlarmRulesTest if they are no longer used: - -```java -// Remove if unused: -// import org.thingsboard.server.common.data.cf.CalculatedField; -// import org.thingsboard.server.common.data.cf.CalculatedFieldType; -``` - -Keep `CalculatedFieldId` import since it's still used by `getDebugEvents`. - -- [ ] **Step 7: Verify test compilation** - -Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn test-compile -pl application -am -q -DskipTests 2>&1 | tail -10` -Expected: BUILD SUCCESS - -- [ ] **Step 8: Run AlarmRulesTest** - -Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn test -pl application -Dtest="org.thingsboard.server.cf.AlarmRulesTest" -DfailIfNoTests=false 2>&1 | tail -30` -Expected: All tests pass. If any test fails, investigate and fix the variable name/type mismatches. - -- [ ] **Step 9: Commit** - -```bash -git add application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java -git add application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java -git commit -m "Update AlarmRulesTest to use new alarm rule API" -``` - ---- - -### Task 5: Create UI AlarmRulesService - -**Files:** -- Create: `ui-ngx/src/app/core/http/alarm-rules.service.ts` - -- [ ] **Step 1: Create the service** - -```typescript -/// -/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL -/// -/// Copyright © 2016-2026 ThingsBoard, Inc. All Rights Reserved. -/// -/// NOTICE: All information contained herein is, and remains -/// the property of ThingsBoard, Inc. and its suppliers, -/// if any. The intellectual and technical concepts contained -/// herein are proprietary to ThingsBoard, Inc. -/// and its suppliers and may be covered by U.S. and Foreign Patents, -/// patents in process, and are protected by trade secret or copyright law. -/// -/// Dissemination of this information or reproduction of this material is strictly forbidden -/// unless prior written permission is obtained from COMPANY. -/// -/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, -/// managers or contractors who have executed Confidentiality and Non-disclosure agreements -/// explicitly covering such access. -/// -/// The copyright notice above does not evidence any actual or intended publication -/// or disclosure of this source code, which includes -/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. -/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, -/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT -/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, -/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. -/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION -/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, -/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. -/// - -import { Injectable } from '@angular/core'; -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, -} from '@shared/models/calculated-field.models'; -import { PageLink } from '@shared/models/page/page-link'; -import { EntityId } from '@shared/models/id/entity-id'; -import { EntityTestScriptResult } from '@shared/models/entity.models'; -import { CalculatedFieldEventBody } from '@shared/models/event.models'; - -@Injectable({ - providedIn: 'root' -}) -export class AlarmRulesService { - - constructor( - private http: HttpClient - ) { } - - public getAlarmRuleById(alarmRuleId: string, config?: RequestConfig): Observable { - return this.http.get(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); - } - - public saveAlarmRule(alarmRule: CalculatedField, config?: RequestConfig): Observable { - return this.http.post('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config)); - } - - public deleteAlarmRule(alarmRuleId: string, config?: RequestConfig): Observable { - return this.http.delete(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); - } - - public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { - return this.http.get>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); - } - - public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/alarm/rule/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); - } - - public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { - return this.http.post('/api/alarm/rule/testScript', inputParams, defaultHttpOptionsFromConfig(config)); - } - - public getLatestAlarmRuleDebugEvent(id: string, config?: RequestConfig): Observable { - return this.http.get(`/api/alarm/rule/${id}/debug`, defaultHttpOptionsFromConfig(config)); - } - - public getAlarmRuleNames(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/alarm/rules/names${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); - } -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add ui-ngx/src/app/core/http/alarm-rules.service.ts -git commit -m "Add AlarmRulesService for UI" -``` - ---- - -### Task 6: Update UI components to use AlarmRulesService - -**Files:** -- Modify: `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts` -- Modify: `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts` -- Modify: `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts` - -- [ ] **Step 1: Update alarm-rules-table-config.ts** - -In `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts`: - -1. Add import for `AlarmRulesService`: -```typescript -import { AlarmRulesService } from '@core/http/alarm-rules.service'; -``` - -2. Change the constructor parameter type from `CalculatedFieldsService` to `AlarmRulesService` (line 105): -```typescript -constructor(private alarmRulesService: AlarmRulesService, -``` - -3. Update all service call sites. Replace: - - `this.calculatedFieldsService.getCalculatedFieldById(id.id)` → `this.alarmRulesService.getAlarmRuleById(id.id)` (line 155) - - `this.calculatedFieldsService.saveCalculatedField(alarmRule)` → `this.alarmRulesService.saveAlarmRule(alarmRule)` (line 156) - - `this.calculatedFieldsService.deleteCalculatedField(id.id)` → `this.alarmRulesService.deleteAlarmRule(id.id)` (line 165) - - `this.calculatedFieldsService.saveCalculatedField(calculatedField)` → `this.alarmRulesService.saveAlarmRule(calculatedField)` (line 393, in `importCalculatedField`) - -4. Update `fetchCalculatedFields` method (line 248-252): -```typescript - fetchCalculatedFields(pageLink: PageLink): Observable> { - return this.pageMode ? - this.alarmRulesService.getAlarmRules(pageLink, this.alarmRuleFilterConfig) : - this.alarmRulesService.getAlarmRulesByEntityId(this.entityId, pageLink); - } -``` - -Note: The `getAlarmRules` call no longer passes `types: [CalculatedFieldType.ALARM]` since the endpoint hardcodes that. The `alarmRuleFilterConfig` query object may still contain entity type filters which will be passed as query params. - -5. Update `onDebugConfigChanged` method (line 414-419): -```typescript - private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { - this.alarmRulesService.getAlarmRuleById(id).pipe( - switchMap(field => this.alarmRulesService.saveAlarmRule({ ...field, debugSettings })), - catchError(() => of(null)), - takeUntilDestroyed(this.destroyRef), - ).subscribe(() => this.updateData()); - } -``` - -6. Remove the `CalculatedFieldsService` import if it's no longer needed. Keep `CalculatedFieldType` import since it's still referenced in `importCalculatedField` for type validation check. - -- [ ] **Step 2: Update alarm-rules-table.component.ts** - -In `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts`: - -1. Replace `CalculatedFieldsService` import with `AlarmRulesService`: -```typescript -import { AlarmRulesService } from '@core/http/alarm-rules.service'; -``` - -2. Change the injected service in the constructor from `CalculatedFieldsService` to `AlarmRulesService`: -```typescript -constructor(private alarmRulesService: AlarmRulesService, -``` - -3. Update the `AlarmRulesTableConfig` instantiation to pass `alarmRulesService` instead of `calculatedFieldsService`: -```typescript -this.alarmRulesTableConfig = new AlarmRulesTableConfig( - this.alarmRulesService, - // ... remaining parameters stay the same -); -``` - -- [ ] **Step 3: Update alarm-rule-dialog.component.ts** - -In `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts`: - -1. Add import for `AlarmRulesService`: -```typescript -import { AlarmRulesService } from '@core/http/alarm-rules.service'; -``` - -2. Change the constructor injection from `CalculatedFieldsService` to `AlarmRulesService`: -```typescript -private alarmRulesService: AlarmRulesService, -``` - -3. Update the `add()` method (around line 206) to use the new service: -```typescript -this.alarmRulesService.saveAlarmRule(alarmRule) -``` - -4. Remove the `CalculatedFieldsService` import if no longer used. - -- [ ] **Step 4: Verify UI build** - -Run: `cd /Users/viacheslav/Desktop/thingsboard-pe/ui-ngx && npx ng build --configuration production 2>&1 | tail -20` - -If the build is slow, alternatively verify just type checking: -Run: `cd /Users/viacheslav/Desktop/thingsboard-pe/ui-ngx && npx tsc --noEmit 2>&1 | head -30` - -Expected: No compilation errors. - -- [ ] **Step 5: Commit** - -```bash -git add ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts -git add ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts -git add ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts -git commit -m "Switch UI alarm rule components to AlarmRulesService" -``` diff --git a/docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md b/docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md deleted file mode 100644 index 3c99e84fb5..0000000000 --- a/docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md +++ /dev/null @@ -1,144 +0,0 @@ -# Alarm Rule Controller Design - -## Problem - -Alarm rules in ThingsBoard PE are internally implemented as calculated fields with `CalculatedFieldType.ALARM`. API consumers must create alarm rules via the `CalculatedFieldController`, setting `type = ALARM` — exposing an implementation detail that is confusing and leaks the CF abstraction. - -## Goal - -Create a dedicated `AlarmRuleController` that provides a clean, alarm-focused REST API while delegating to the existing `TbCalculatedFieldService` underneath. Update the UI and tests to use the new endpoints. - -## Approach - -Thin controller wrapper: `AlarmRuleController` delegates directly to `TbCalculatedFieldService`. A new `AlarmRuleDefinition` DTO wraps `CalculatedField` with the `type` field hidden (always ALARM). Conversion happens inline in the controller. No new service layer. - -The existing `CalculatedFieldController` remains unchanged. - -## DTOs - -### AlarmRuleDefinition - -Located in `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java`. - -Same structure as `CalculatedField` but without the `type` field: - -- `id`: `CalculatedFieldId` (reused, not a new ID type) -- `tenantId`: `TenantId` -- `name`: `String` -- `entityId`: `EntityId` -- `configuration`: `AlarmCalculatedFieldConfiguration` -- `createdTime`: `long` -- `configurationVersion`: `int` - -Provides conversion methods: -- `toCalculatedField()` — sets `type = ALARM`, copies all fields -- `static fromCalculatedField(CalculatedField cf)` — strips `type`, copies all fields - -### AlarmRuleDefinitionInfo - -Extends `AlarmRuleDefinition`, adds `entityName: String`. Mirrors the `CalculatedFieldInfo` pattern. - -Provides: -- `static fromCalculatedFieldInfo(CalculatedFieldInfo cfi)` — converts from the existing info type - -## Controller - -### AlarmRuleController - -Located in `application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java`. - -- `@RequestMapping("/api/alarm/rule")` for single-entity operations -- Extends `BaseController` -- Injects `TbCalculatedFieldService`, `EventService`, `TbelInvokeService` -- All endpoints require `TENANT_ADMIN` authority -- Uses existing `Operation.WRITE_CALCULATED_FIELD` / `READ_CALCULATED_FIELD` permissions - -### Endpoints - -| Method | Path | Description | Delegates To | -|--------|------|-------------|--------------| -| `POST` | `/api/alarm/rule` | Create or update alarm rule | `TbCalculatedFieldService.save()` | -| `GET` | `/api/alarm/rule/{alarmRuleId}` | Get alarm rule by ID | `TbCalculatedFieldService.findById()` | -| `GET` | `/api/alarm/rule/{entityType}/{entityId}` | Get alarm rules by entity (paged) | `TbCalculatedFieldService.findByTenantIdAndEntityId()` with `type=ALARM` | -| `GET` | `/api/alarm/rules` | List all alarm rules (paged, filtered) | `calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter()` with `types={ALARM}` | -| `GET` | `/api/alarm/rules/names` | Get alarm rule names (paged) | `calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType()` with `type=ALARM` | -| `DELETE` | `/api/alarm/rule/{alarmRuleId}` | Delete alarm rule | `TbCalculatedFieldService.delete()` | -| `GET` | `/api/alarm/rule/{alarmRuleId}/debug` | Get latest debug event | `EventService.findLatestEvents()` | -| `POST` | `/api/alarm/rule/testScript` | Test TBEL expression | Same logic as CF testScript | - -### Save endpoint details - -Accepts `AlarmRuleDefinition`. The controller: -1. Converts to `CalculatedField` via `toCalculatedField()` (sets `type = ALARM`) -2. Sets `tenantId` from the current user -3. Checks entity permissions (`Operation.WRITE_CALCULATED_FIELD`) -4. Checks referenced entities in the configuration -5. Calls `TbCalculatedFieldService.save()` -6. Converts result back to `AlarmRuleDefinition` - -### List endpoint details - -Accepts the same filter parameters as the CF list endpoint (entity type, entity IDs, text search, paging) but: -- Does NOT accept `types` parameter (hardcoded to `{ALARM}`) -- Does NOT accept `name` parameter (from the CF endpoint's multi-value `name` query param) -- Returns `PageData` (converted from `PageData`) -- Entity type filter defaults to all entity types that support ALARM (DEVICE, ASSET, CUSTOMER, DEVICE_PROFILE, ASSET_PROFILE), with the same permission check pattern as the CF controller - -### Get by entity endpoint details - -Accepts `entityType`, `entityId`, paging parameters. Does NOT accept `type` parameter (hardcoded to `ALARM`). Returns `PageData`. - -## UI Changes - -### New service: alarm-rules.service.ts - -Located in `ui-ngx/src/app/core/http/alarm-rules.service.ts`. - -Follows the same `@Injectable({ providedIn: 'root' })` pattern as `CalculatedFieldsService`. Methods: - -- `saveAlarmRule(rule: AlarmRuleDefinition)` -> `POST /api/alarm/rule` -- `getAlarmRuleById(id: string)` -> `GET /api/alarm/rule/{id}` -- `getAlarmRulesByEntityId(entityId: EntityId, pageLink: PageLink)` -> `GET /api/alarm/rule/{entityType}/{entityId}` -- `getAlarmRules(pageLink: PageLink, query)` -> `GET /api/alarm/rules` -- `getAlarmRuleNames(pageLink: PageLink)` -> `GET /api/alarm/rules/names` -- `deleteAlarmRule(id: string)` -> `DELETE /api/alarm/rule/{id}` -- `getLatestAlarmRuleDebugEvent(id: string)` -> `GET /api/alarm/rule/{id}/debug` -- `testScript(inputParams)` -> `POST /api/alarm/rule/testScript` - -### Consumer updates - -All components that currently call `CalculatedFieldsService` for ALARM-type operations switch to `AlarmRulesService`. Primary consumers: - -- `AlarmRuleDialogComponent` — switches `saveCalculatedField()` to `saveAlarmRule()` -- Any component loading alarm rules by entity — switches to `getAlarmRulesByEntityId()` -- Components listing alarm rules — switches to `getAlarmRules()` - -The TypeScript model type `CalculatedFieldAlarmRule` can be updated or aliased to match the new `AlarmRuleDefinition` shape (without the `type` discriminator field in the request). - -## Test Changes - -### AlarmRulesTest.java - -Located at `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java`. - -Updates: -- `saveCalculatedField()` helper changes from `POST /api/calculatedField` to `POST /api/alarm/rule` -- The helper builds `AlarmRuleDefinition` instead of `CalculatedField` (no `type` field, alarm configuration directly on the object) -- GET calls for alarm rules by entity change to `/api/alarm/rule/{entityType}/{entityId}` -- Debug event retrieval may optionally use `/api/alarm/rule/{id}/debug` instead of direct `EventDao` access (though the test currently uses `EventDao` directly for most checks, which is fine) -- The `createAlarmCf()` helper is renamed to `createAlarmRule()` and returns `AlarmRuleDefinition` - -## Scope boundaries - -**In scope:** -- New `AlarmRuleDefinition` and `AlarmRuleDefinitionInfo` DTOs -- New `AlarmRuleController` with all listed endpoints -- New `AlarmRulesService` on the UI side -- Update `AlarmRulesTest` to use new API -- Update UI components to use new service - -**Out of scope:** -- Changes to `CalculatedFieldController` (left as-is) -- New permission types (reuses `READ_CALCULATED_FIELD` / `WRITE_CALCULATED_FIELD`) -- Reprocessing endpoints (not applicable to alarm rules) -- New `TbAlarmRuleService` layer (thin wrapper approach, no new service) From fba53d602e8df7368638fe8d9997b9a9996c87a3 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 31 Mar 2026 11:36:09 +0200 Subject: [PATCH 208/313] Additional fixes --- .../alarm-rule-filter-config.component.html | 50 ----------------- .../alarm/alarm-comment-dialog.component.html | 3 +- .../alarm/alarm-filter-config.component.html | 55 ------------------- .../add-attribute-dialog.component.html | 3 +- ...ulated-fields-filter-config.component.html | 52 ------------------ .../device/device-info-filter.component.html | 36 ------------ .../entity/add-entity-dialog.component.html | 3 +- .../asset-profile-dialog.component.html | 3 +- .../device-profile-dialog.component.html | 3 +- ...object-add-instances-dialog.component.html | 3 +- .../tenant-profile-dialog.component.html | 3 +- .../relation/relation-dialog.component.html | 3 +- .../resources/resources-dialog.component.html | 3 +- ...anage-widget-actions-dialog.component.html | 3 +- .../embed-dashboard-dialog.component.html | 3 +- .../select-entity-dialog.component.html | 3 +- .../rpc/persistent-add-dialog.component.html | 3 +- .../persistent-details-dialog.component.html | 3 +- .../key/data-key-config-dialog.component.html | 3 +- ...entities-to-customer-dialog.component.html | 3 +- ...add-entities-to-edge-dialog.component.html | 3 +- .../assign-to-customer-dialog.component.html | 3 +- .../admin/general-settings.component.html | 6 +- .../clients/client-dialog.component.html | 3 +- .../admin/security-settings.component.html | 3 +- .../admin/send-test-sms-dialog.component.html | 3 +- ...mport-dashboard-file-dialog.component.html | 3 +- ...ake-dashboard-public-dialog.component.html | 3 +- ...-dashboard-customers-dialog.component.html | 3 +- .../mobile-app-dialog.component.html | 3 +- ...ile-qr-code-widget-settings.component.html | 3 +- .../activation-link-dialog.component.html | 3 +- .../pages/user/add-user-dialog.component.html | 3 +- .../save-widget-type-as-dialog.component.html | 3 +- .../select-widget-type-dialog.component.html | 3 +- .../widgets-bundle-dialog.component.html | 3 +- .../image/image-dialog.component.html | 3 +- .../image/upload-image-dialog.component.html | 3 +- 38 files changed, 35 insertions(+), 263 deletions(-) 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 e65b8919ce..929e7d581e 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 @@ -67,56 +67,6 @@ } } - - @if (buttonMode) { - - } @else { -
-
-
alarm-rule.alarm-rule-type-list
- - -
-
-
entity.entity-type
- - - {{ 'alarm-rule.any-type' | translate }} - @for (type of listEntityTypes; track type) { - - {{ entityTypeTranslations.get(type)?.type | translate }} - - } - - -
- @if (alarmRuleFilterConfigForm.get('entityType').value) { -
-
entity.entities
- - -
- } -
- } -
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html index 5f18a32d62..a5935e1f2b 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html @@ -25,8 +25,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html index 839dc49238..ca1578c054 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html @@ -72,61 +72,6 @@
} } - - @if (buttonMode) { - - } @else { -
-
-
alarm.alarm-status-list
- - @for (searchStatus of alarmSearchStatuses; track searchStatus) { - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - } - -
-
-
alarm.alarm-severity-list
- - @for (alarmSeverity of alarmSeverities; track alarmSeverity) { - - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - - } - -
-
-
alarm.alarm-type-list
- - -
-
-
alarm.assignee
- - -
- @if (propagatedFilter) { - - {{ 'alarm.search-propagated-alarms' | translate }} - - } -
- } -
diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index 7028814b79..eddcf3f9b2 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
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 74075f56ee..79062b1a0e 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 @@ -69,58 +69,6 @@
} } - - @if (buttonMode) { - - } @else { -
-
-
calculated-fields.calculated-field-types
- - -
-
-
entity.entity-type
- - - {{ 'alarm-rule.any-type' | translate }} - @for (type of listEntityTypes; track type) { - - {{ entityTypeTranslations.get(type)?.type | translate }} - - } - - -
- @if (cfFilterForm.get('entityType').value) { -
-
entity.entities
- - -
- } -
- } -
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 3fe16989cf..0029a3ba3f 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 @@ -53,42 +53,6 @@ } } - - @if (buttonMode) { - - } @else { -
-
-
device-profile.device-profile
- - -
-
-
device.device-state
- - - {{ 'device.any' | translate }} - {{ 'device.active' | translate }} - {{ 'device.inactive' | translate }} - - -
-
- } -
diff --git a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html index b10c1b89db..795bde21c1 100644 --- a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html @@ -29,8 +29,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html index 011b703fee..d47b4fa65c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html index a180367e77..dcb15e48b8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html index 6f63601f0b..1f36c01fe4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html index 037e7eec22..19ef843762 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html index 1bb6e97703..896adb0c46 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html index 7ab487e78c..a5757e7e58 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html index c97d715c41..8b0ca22543 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html index 1b523be28d..a31ad603f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html @@ -26,8 +26,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html index 813d32609f..afc11a7633 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/dialogs/select-entity-dialog.component.html @@ -26,8 +26,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html index f8d8b7c0e3..5f7d83fbad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html index af0e848655..bffb4e21cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html index bee0b3ab1c..802adb5211 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html @@ -34,8 +34,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html index eb07e82ce8..c397f8b1a0 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html index c95d297864..599b217e61 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html index 3344e70837..1e105ff86a 100644 --- a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html index 4c9971bb42..e6dc6b74b9 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html @@ -26,8 +26,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
} @@ -72,8 +71,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html index 31c40945e1..fc26b51e86 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html @@ -29,8 +29,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html index 5ee5643d76..b3b126b5d7 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html @@ -26,8 +26,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
} diff --git a/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html b/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html index b9fddd605d..3245013330 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/send-test-sms-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html index 0cebbc0286..3286044628 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html index 518115993a..e21f996584 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.html index ba5c6eb2f1..f53a8b37ca 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.html @@ -29,8 +29,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html index ab4417c23b..79bd13c8b1 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.html @@ -29,8 +29,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html index 83419e2191..5a4cc4599a 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html @@ -25,8 +25,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
} diff --git a/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html index a5a6042e2e..c8e1b97f5f 100644 --- a/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html index d2e86b8779..60ae3ad1c2 100644 --- a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html @@ -29,8 +29,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html index 856de848d1..b76b922008 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html index f0ca976d37..60ecf4ea6d 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html @@ -29,8 +29,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html index dd4b56bef8..c3a965b752 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.html b/ui-ngx/src/app/shared/components/image/image-dialog.component.html index d0ff71778c..ee830aa102 100644 --- a/ui-ngx/src/app/shared/components/image/image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
diff --git a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html index bc1c5c2cfb..edde51a8a7 100644 --- a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { - } - @if (!(isLoading$ | async)) { + } @else {
}
From 8f8296e63b7673532d0da0f4351a6f83cf3761a4 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 31 Mar 2026 11:38:34 +0200 Subject: [PATCH 209/313] if block fix --- .../home/components/ai-model/ai-model-dialog.component.html | 3 +-- .../pages/mobile/applications/remove-app-dialog.component.html | 3 +-- .../pages/mobile/bundes/mobile-bundle-dialog.component.html | 3 +-- .../backup-code-auth-dialog.component.html | 3 +-- .../authentication-dialog/email-auth-dialog.component.html | 3 +-- .../authentication-dialog/sms-auth-dialog.component.html | 3 +-- .../authentication-dialog/totp-auth-dialog.component.html | 3 +-- .../shared/components/image/embed-image-dialog.component.html | 3 +-- 8 files changed, 8 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 7c4d8a96a5..6802eaaf8b 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -28,8 +28,7 @@ @if (isLoading$ | async) { -} -@if (!(isLoading$ | async)) { +} @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html index c35758d385..d02a692247 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/remove-app-dialog.component.html @@ -26,8 +26,7 @@ @if (isLoading$ | async) { -} -@if (!(isLoading$ | async)) { +} @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html index 11b137d6a9..738dd372b7 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -27,8 +27,7 @@ @if (isLoading$ | async) { -} -@if (!(isLoading$ | async)) { +} @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html index db8e6b1cae..bb0eace707 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html @@ -27,8 +27,7 @@ @if (isLoading$ | async) { -} -@if (!(isLoading$ | async)) { +} @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html index 4a4e29ccf7..669fe49250 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html @@ -27,8 +27,7 @@ @if (isLoading$ | async) { -} -@if (!(isLoading$ | async)) { +} @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html index 88784ead77..fa4b78de70 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html @@ -27,8 +27,7 @@ @if (isLoading$ | async) { -} -@if (!(isLoading$ | async)) { +} @else {
}
diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html index df8b600841..39b4bf3762 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html @@ -27,8 +27,7 @@ @if (isLoading$ | async) { -} -@if (!(isLoading$ | async)) { +} @else {
}
diff --git a/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html index c9446437b7..c5a1a749ef 100644 --- a/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html @@ -27,8 +27,7 @@ @if (isLoading$ | async) { -} -@if (!(isLoading$ | async)) { +} @else {
}
From d2b0cd049f6ac6c5aef4caf8680d57c6b71a9cc7 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 31 Mar 2026 12:44:32 +0300 Subject: [PATCH 210/313] Use plural path for alarm rules by entity endpoint --- .../org/thingsboard/server/controller/AlarmRuleController.java | 2 +- ui-ngx/src/app/core/http/alarm-rules.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java index 243f311b49..646ab3dee7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java @@ -150,7 +150,7 @@ public class AlarmRuleController extends BaseController { @ApiOperation(value = "Get Alarm Rules by Entity Id (getAlarmRulesByEntityId)", notes = "Fetch the Alarm Rules based on the provided Entity Id." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/alarm/rule/{entityType}/{entityId}") + @GetMapping(value = "/alarm/rules/{entityType}/{entityId}") public PageData getAlarmRulesByEntityId( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, diff --git a/ui-ngx/src/app/core/http/alarm-rules.service.ts b/ui-ngx/src/app/core/http/alarm-rules.service.ts index 1800b2a475..2ab7857793 100644 --- a/ui-ngx/src/app/core/http/alarm-rules.service.ts +++ b/ui-ngx/src/app/core/http/alarm-rules.service.ts @@ -56,7 +56,7 @@ export class AlarmRulesService { } public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/alarm/rule/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + return this.http.get>(`/api/alarm/rules/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { From 6028288e323c1491e0c7ad8c856cf1478ed13011 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 31 Mar 2026 12:55:58 +0300 Subject: [PATCH 211/313] Address PR review: add type guard, null checks, and input validation --- .../controller/AlarmRuleController.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java index 646ab3dee7..d2e9adf3ea 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java @@ -43,6 +43,7 @@ import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; @@ -141,8 +142,7 @@ public class AlarmRuleController extends BaseController { public AlarmRuleDefinition getAlarmRuleById(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { checkParameter(ALARM_RULE_ID, strAlarmRuleId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); - CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); - checkNotNull(calculatedField); + CalculatedField calculatedField = checkAlarmRule(calculatedFieldId); checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); return AlarmRuleDefinition.fromCalculatedField(calculatedField); } @@ -233,7 +233,7 @@ public class AlarmRuleController extends BaseController { public void deleteAlarmRule(@PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws Exception { checkParameter(ALARM_RULE_ID, strAlarmRuleId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); - CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + CalculatedField calculatedField = checkAlarmRule(calculatedFieldId); checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); tbCalculatedFieldService.delete(calculatedField, getCurrentUser()); } @@ -246,7 +246,7 @@ public class AlarmRuleController extends BaseController { public JsonNode getLatestAlarmRuleDebugEvent(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { checkParameter(ALARM_RULE_ID, strAlarmRuleId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); - CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + CalculatedField calculatedField = checkAlarmRule(calculatedFieldId); checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); TenantId tenantId = getCurrentUser().getTenantId(); return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1)) @@ -260,7 +260,8 @@ public class AlarmRuleController extends BaseController { @PostMapping("/alarm/rule/testScript") public JsonNode testAlarmRuleScript( @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL condition expression. The expression must return a boolean value.") - @RequestBody JsonNode inputParams) { + @RequestBody JsonNode inputParams) throws ThingsboardException { + checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null); String expression = inputParams.get("expression").asText(); Map arguments = Objects.requireNonNullElse( JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), @@ -314,4 +315,13 @@ public class AlarmRuleController extends BaseController { .put("error", errorText); } + private CalculatedField checkAlarmRule(CalculatedFieldId calculatedFieldId) throws ThingsboardException { + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + checkNotNull(calculatedField); + if (calculatedField.getType() != CalculatedFieldType.ALARM) { + throw new ThingsboardException("Alarm rule not found", ThingsboardErrorCode.ITEM_NOT_FOUND); + } + return calculatedField; + } + } From e6771bb9d39a6bcd10bfe22b709108ed2ae072cf Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 31 Mar 2026 13:26:50 +0300 Subject: [PATCH 212/313] Add missing EdgeVersion entries V_4_2_2_2 and V_4_3_1_2 --- common/edge-api/src/main/proto/edge.proto | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 48ec26f875..bcea3c30d9 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -48,9 +48,11 @@ enum EdgeVersion { V_4_2_1_2 = 14; V_4_2_2 = 4220; V_4_2_2_1 = 4221; + V_4_2_2_2 = 4222; V_4_3_0_1 = 15; V_4_3_1 = 4310; V_4_3_1_1 = 4311; + V_4_3_1_2 = 4312; V_4_4_0 = 4400; V_LATEST = 99999; From ae58a58a6a3995dafadac36df5a098e8b38bdd8f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 31 Mar 2026 16:59:04 +0300 Subject: [PATCH 213/313] Refactor filter dialog components: typed forms, modern Angular patterns, UI improvements - Migrate filter-dialog, key-filter-list, user-filter-dialog to typed reactive forms - Replace mat-expansion-panel with tb-form-panel stroked layout in key-filter-list - Move AND/OR toggle-select into key-filter-list via ng-content projection - Replace editable slide-toggle with tb-rounded-btn toggle button - Migrate to @if/@for/@switch control flow and takeUntilDestroyed - Remove unnecessary ErrorStateMatcher/submitted from dialog components - Simplify keyFiltersToText operation parameter and alias-controller filter resolution - Fix filter-text ngOnChanges to check value equality - Fix user-filter-dialog: pass label to tb-datetime, fix tbTruncateWithTooltip in slide-toggle --- ui-ngx/src/app/core/api/alias-controller.ts | 10 +- .../app/core/api/entity-data-subscription.ts | 1 - .../filter/filter-dialog.component.html | 81 +++++----- .../filter/filter-dialog.component.scss | 11 -- .../filter/filter-dialog.component.ts | 81 +++++----- .../filter/filter-text.component.ts | 56 +++---- .../filter/key-filter-list.component.html | 144 +++++++++--------- .../filter/key-filter-list.component.scss | 51 ++++--- .../filter/key-filter-list.component.ts | 122 +++++++-------- .../filter/user-filter-dialog.component.html | 77 +++++----- .../filter/user-filter-dialog.component.scss | 6 +- .../filter/user-filter-dialog.component.ts | 68 ++++----- .../components/time/datetime.component.ts | 2 +- .../app/shared/models/query/query.models.ts | 3 +- 14 files changed, 333 insertions(+), 380 deletions(-) diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index d48bcb574b..de6863ee56 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -314,10 +314,14 @@ export class AliasController implements IAliasController { || newDatasource.type === DatasourceType.entityCount || newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.filterId) { - newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); const filterInfo = this.getFilterInfo(newDatasource.filterId); - if (filterInfo?.keyFiltersOperation) { - newDatasource.keyFiltersOperation = filterInfo.keyFiltersOperation; + if (filterInfo) { + newDatasource.keyFilters = filterInfoToKeyFilters(filterInfo); + if (filterInfo.keyFiltersOperation) { + newDatasource.keyFiltersOperation = filterInfo.keyFiltersOperation; + } + } else { + newDatasource.keyFilters = []; } } if (newDatasource.type === DatasourceType.alarmCount) { diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index e367ae8faa..6f7949d259 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -358,7 +358,6 @@ export class EntityDataSubscription { this.subscriber = new TelemetrySubscriber(this.telemetryService); this.dataCommand = new EntityDataCmd(); - let keyFilters = this.entityDataSubscriptionOptions.keyFilters; if (this.entityDataSubscriptionOptions.additionalKeyFilters) { if (keyFilters) { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html index 6865bfd9c3..0cd0ea729e 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html @@ -16,64 +16,61 @@ --> - +

{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}

-
- - -
-
-
-
- - filter.name - - - {{ 'filter.name-required' | translate }} - - - {{ 'filter.duplicate-filter' | translate }} - - -
- - - -
-
-
-
filter.key-filters
- - {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} - {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }} - -
- - -
-
+
+
+ + filter.name + + @if (filterFormGroup.controls.filter.hasError('required')) { + {{ 'filter.name-required' | translate }} + } + @if (filterFormGroup.controls.filter.hasError('duplicateFilterName')) { + {{ 'filter.duplicate-filter' | translate }} + } + + +
+ + + {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate | titlecase }} + + {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate | titlecase }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss index 7b620f8c2f..2fd238043d 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.scss @@ -14,15 +14,4 @@ * limitations under the License. */ :host { - .tb-editable-switch { - padding-left: 10px; - - .editable-switch { - margin: 0; - } - - .editable-label { - margin: 5px 0; - } - } } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts index 334ce90931..6db4a76822 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts @@ -14,26 +14,23 @@ /// limitations under the License. /// -import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; -import { ErrorStateMatcher } from '@angular/material/core'; +import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, - FormGroupDirective, - NgForm, - ValidatorFn, - Validators -} from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { UtilsService } from '@core/services/utils.service'; -import { TranslateService } from '@ngx-translate/core'; -import { ComplexOperation, complexOperationTranslationMap, Filter, Filters } from '@shared/models/query/query.models'; +import { + ComplexOperation, + complexOperationTranslationMap, + Filter, + FilterInfo, + Filters +} from '@shared/models/query/query.models'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { FormControlsFrom } from '@shared/models/tenant.model'; export interface FilterDialogData { isAdd: boolean; @@ -42,36 +39,30 @@ export interface FilterDialogData { } @Component({ - selector: 'tb-filter-dialog', - templateUrl: './filter-dialog.component.html', - providers: [{ provide: ErrorStateMatcher, useExisting: FilterDialogComponent }], - styleUrls: ['./filter-dialog.component.scss'], - standalone: false + selector: 'tb-filter-dialog', + templateUrl: './filter-dialog.component.html', + styleUrls: ['./filter-dialog.component.scss'], + standalone: false }) -export class FilterDialogComponent extends DialogComponent - implements OnInit, ErrorStateMatcher { +export class FilterDialogComponent extends DialogComponent { isAdd: boolean; - filters: Array; - - filter: Filter; - - filterFormGroup: UntypedFormGroup; - submitted = false; + filterFormGroup: FormGroup>; ComplexOperation = ComplexOperation; complexOperationTranslationMap = complexOperationTranslationMap; allowKeyFiltersOrConditions: boolean; + private readonly filter: Filter; + private filters: Array; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: FilterDialogData, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public dialogRef: MatDialogRef, - private fb: UntypedFormBuilder, - private utils: UtilsService, - public translate: TranslateService) { + protected dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService) { super(store, router, dialogRef); this.isAdd = data.isAdd; if (Array.isArray(data.filters)) { @@ -102,15 +93,15 @@ export class FilterDialogComponent extends DialogComponent { + return (c: FormControl) => { const newFilter = c.value.trim(); const found = this.filters.find((filter) => filter.filter === newFilter); if (found) { @@ -126,13 +117,11 @@ export class FilterDialogComponent extends DialogComponent FilterTextComponent), - multi: true - } - ], - standalone: false + selector: 'tb-filter-text', + templateUrl: './filter-text.component.html', + styleUrls: ['./filter-text.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterTextComponent), + multi: true + } + ], + standalone: false }) -export class FilterTextComponent implements ControlValueAccessor, OnInit, OnChanges { +export class FilterTextComponent implements ControlValueAccessor, OnChanges { - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required = false; @Input() disabled: boolean; @@ -66,26 +60,20 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit, OnChan public filterText: string; private currentValue: Array; - private propagateChange = (v: any) => { }; - constructor(private dialog: MatDialog, - private fb: UntypedFormBuilder, - private translate: TranslateService, + constructor(private translate: TranslateService, private datePipe: DatePipe) { } - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { + registerOnChange(_fn: any): void { } - ngOnInit() { + registerOnTouched(_fn: any): void { } ngOnChanges(changes: SimpleChanges) { - if (changes.operation && !changes.operation.firstChange) { + if (changes.operation && !changes.operation.firstChange + && changes.operation.currentValue !== changes.operation.previousValue) { this.updateFilterText(this.currentValue); } } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html index a772c765fd..8bfd7792d5 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html @@ -15,78 +15,86 @@ limitations under the License. --> -
- - - - -
filter.key-filters
-
-
-
- -
- - -   -   -
-
- -
-
-
- {{ complexOperationTranslationMap.get(operation) | translate }} +
+
+
+
filter.key-filters
+ +
+ @if (keyFiltersFormArray.length) { +
+
+ +
+
filter.key-name
+
filter.key-type.key-type
+   +  
-
-
-
{{ keyFilterControl.value.key.key }}
-
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}
- - +
+ +
+ @for (keyFilterControl of keyFiltersFormArray.controls; track keyFilterControl; let index = $index) { +
+
+ @if (index) { +
+ {{ complexOperationTranslationMap.get(operation) | translate }} +
+ } +
+
+
+
{{ keyFilterControl.value.key.key }}
+
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}
+ + +
+
- -
+ }
- filter.no-key-filters -
-
- -
- - +
+ } @else { + filter.no-key-filters + } +
+ +
+
+
+ - -
filter.preview
-
+ {{ 'filter.preview' | translate }}
-
- -
+ + @if (keyFiltersFormArray.length) { + + } @else { + filter.no-filter-text + } +
- +
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss index 2d1bcc923a..9ef70c8845 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss @@ -13,30 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../scss/constants"; + :host { - .key-filter-list { + .filter-title { + padding: 12px 0; + font-size: 14px; + font-weight: 500; + } + .no-data-found { + height: 50px; + font-size: 16px; + } + .filter-list { overflow: auto; max-height: 300px; - .no-data-found { - height: 50px; + + &-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); + } + + .filters-text { + font-size: 14px; } } .filters-operation { - margin-top: -18px; - color: #666; - font-weight: 500; - } - .tb-filter-preview { - padding: 8px; - border: 1px groove rgba(0, 0, 0, .25); - border-radius: 4px; - } -} - -:host ::ng-deep { - .tb-filter-preview { - .tb-filter-text { - max-height: 200px; + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + background-color: white; + } + &-label { + font-size: 15px; + font-weight: 400; + color: $tb-primary-color; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba($tb-primary-color, 0.32); + background-color: rgba($tb-primary-color, 0.04); } } } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts index 8951764a4b..73804faa7a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts @@ -14,26 +14,26 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { - AbstractControl, ControlValueAccessor, - UntypedFormArray, - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, + FormArray, + FormBuilder, + FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, Validators } from '@angular/forms'; -import { Observable, Subject } from 'rxjs'; +import { Observable } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ComplexOperation, complexOperationTranslationMap, EntityKeyType, entityKeyTypeTranslationMap, + KeyFilter, KeyFilterInfo, keyFilterInfosToKeyFilters } from '@shared/models/query/query.models'; @@ -41,27 +41,26 @@ import { MatDialog } from '@angular/material/dialog'; import { deepClone } from '@core/utils'; import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; import { EntityId } from '@shared/models/id/entity-id'; -import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-key-filter-list', - templateUrl: './key-filter-list.component.html', - styleUrls: ['./key-filter-list.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => KeyFilterListComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => KeyFilterListComponent), - multi: true - } - ], - standalone: false + selector: 'tb-key-filter-list', + templateUrl: './key-filter-list.component.html', + styleUrls: ['./key-filter-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KeyFilterListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KeyFilterListComponent), + multi: true + } + ], + standalone: false }) -export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { +export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -77,39 +76,28 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator, complexOperationTranslationMap = complexOperationTranslationMap; - keyFilterListFormGroup: UntypedFormGroup; - entityKeyTypeTranslations = entityKeyTypeTranslationMap; - keyFiltersControl: UntypedFormControl; + keyFiltersFormArray: FormArray>; + + keyFiltersControl: FormControl>; - private destroy$ = new Subject(); private propagateChange = null; - constructor(private fb: UntypedFormBuilder, - private dialog: MatDialog) { + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { } ngOnInit(): void { - this.keyFilterListFormGroup = this.fb.group({ - keyFilters: this.fb.array([]) - }); + this.keyFiltersFormArray = this.fb.array>([]); this.keyFiltersControl = this.fb.control(null); - this.keyFilterListFormGroup.valueChanges.pipe( - takeUntil(this.destroy$) + this.keyFiltersFormArray.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) ).subscribe(() => this.updateModel()); } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - get keyFiltersFormArray(): UntypedFormArray { - return this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray; - } - registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -120,61 +108,61 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator, setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { - this.keyFilterListFormGroup.disable({emitEvent: false}); + this.keyFiltersFormArray.disable({emitEvent: false}); this.keyFiltersControl.disable({emitEvent: false}); } else { - this.keyFilterListFormGroup.enable({emitEvent: false}); + this.keyFiltersFormArray.enable({emitEvent: false}); this.keyFiltersControl.enable({emitEvent: false}); } } validate(): ValidationErrors | null { - return this.keyFilterListFormGroup.valid && this.keyFiltersControl.valid ? null : { + return this.keyFiltersFormArray.valid && this.keyFiltersControl.valid ? null : { keyFilterList: {valid: false} }; } writeValue(keyFilters: Array): void { - if (keyFilters.length === this.keyFiltersFormArray.length) { + if (keyFilters?.length === this.keyFiltersFormArray.length) { this.keyFiltersFormArray.patchValue(keyFilters, {emitEvent: false}); } else { - const keyFilterControls: Array = []; + this.keyFiltersFormArray.clear({emitEvent: false}); if (keyFilters) { for (const keyFilter of keyFilters) { - keyFilterControls.push(this.fb.control(keyFilter, [Validators.required])); + this.keyFiltersFormArray.push( + this.fb.control(keyFilter, [Validators.required]), + {emitEvent: false} + ); } } - this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls), {emitEvent: false}); if (this.disabled) { - this.keyFilterListFormGroup.disable({emitEvent: false}); + this.keyFiltersFormArray.disable({emitEvent: false}); } else { - this.keyFilterListFormGroup.enable({emitEvent: false}); + this.keyFiltersFormArray.enable({emitEvent: false}); } } const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); } - public removeKeyFilter(index: number) { - (this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).removeAt(index); + removeKeyFilter(index: number) { + this.keyFiltersFormArray.removeAt(index); } - public addKeyFilter() { - const keyFiltersFormArray = this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray; + addKeyFilter() { this.openKeyFilterDialog(null).subscribe((result) => { if (result) { - keyFiltersFormArray.push(this.fb.control(result, [Validators.required])); + this.keyFiltersFormArray.push(this.fb.control(result, [Validators.required])); } }); } - public editKeyFilter(index: number) { - const keyFilter: KeyFilterInfo = - (this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).at(index).value; + editKeyFilter(index: number) { + const keyFilter = this.keyFiltersFormArray.at(index).value; this.openKeyFilterDialog(keyFilter).subscribe( (result) => { if (result) { - (this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray).at(index).patchValue(result); + this.keyFiltersFormArray.at(index).patchValue(result); } } ); @@ -210,12 +198,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator, } private updateModel() { - const keyFilters: Array = this.keyFilterListFormGroup.getRawValue().keyFilters; - if (keyFilters.length) { - this.propagateChange(keyFilters); - } else { - this.propagateChange(null); - } + const keyFilters = this.keyFiltersFormArray.getRawValue(); + this.propagateChange(keyFilters.length ? keyFilters : null); const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html index 8ca43427e9..f1836ae17e 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html @@ -15,64 +15,61 @@ limitations under the License. --> - - + +

{{ filter.filter | customTranslate }}

-
- -
-
-
-
- - - {{ userInputControl.get('label').value | customTranslate }} - - - - - - {{ userInputControl.get('label').value | customTranslate }} - -
- {{ userInputControl.get('unitSymbol').value }} + @for (userInputControl of userInputsFormArray.controls; track userInputControl) { + @switch (userInputControl.controls.valueType.value) { + @case (valueTypeEnum.STRING) { + + {{ userInputControl.controls.label.value | customTranslate }} + + + } + @case (valueTypeEnum.NUMERIC) { + + {{ userInputControl.controls.label.value | customTranslate }} + + @if (userInputControl.controls.unitSymbol.value) { +
+ {{ userInputControl.controls.unitSymbol.value }}
-
- - - - - - - - {{ userInputControl.get('label').value | customTranslate }} - - -
-
-
+ } + + } + @case (valueTypeEnum.DATE_TIME) { + + } + @case (valueTypeEnum.BOOLEAN) { + + + {{ userInputControl.controls.label.value | customTranslate }} + + + } + } + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss index d3b1d316ab..552c49b56a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.scss @@ -14,14 +14,14 @@ * limitations under the License. */ :host ::ng-deep { - .user-filter-dialog { - .mat-mdc-form-field.mat-mdc-form-field-has-icon-suffix.mat-form-field-hide-placeholder { + .numeric-field { + &.mat-mdc-form-field-has-icon-suffix.mat-form-field-hide-placeholder { .mat-mdc-form-field-icon-suffix { place-self: center; } } - .mat-mdc-form-field.mat-mdc-form-field-has-icon-suffix { + &.mat-mdc-form-field-has-icon-suffix { .mat-mdc-form-field-icon-suffix { place-self: baseline; } diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts index 6e068fdcf3..59231f8b80 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts @@ -14,12 +14,11 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject, SkipSelf } from '@angular/core'; -import { ErrorStateMatcher } from '@angular/material/core'; +import { Component, DestroyRef, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormArray, FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { TranslateService } from '@ngx-translate/core'; @@ -39,29 +38,31 @@ export interface UserFilterDialogData { filter: Filter; } +interface UserInputForm { + label: FormControl; + valueType: FormControl; + unitSymbol: FormControl; + value: FormControl; +} + @Component({ selector: 'tb-user-filter-dialog', templateUrl: './user-filter-dialog.component.html', - providers: [{ provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent }], styleUrls: ['./user-filter-dialog.component.scss'], standalone: false }) -export class UserFilterDialogComponent extends DialogComponent - implements ErrorStateMatcher { +export class UserFilterDialogComponent extends DialogComponent { filter: Filter; - userFilterFormGroup: FormGroup; + userInputsFormArray: FormArray>; valueTypeEnum = EntityKeyValueType; - submitted = false; - constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: UserFilterDialogData, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public dialogRef: MatDialogRef, + protected dialogRef: MatDialogRef, private fb: FormBuilder, private translate: TranslateService, private destroyRef: DestroyRef, @@ -70,19 +71,13 @@ export class UserFilterDialogComponent extends DialogComponent = []; - for (const userInput of userInputs) { - userInputControls.push(this.createUserInputFormControl(userInput)); - } - - this.userFilterFormGroup = this.fb.group({ - userInputs: this.fb.array(userInputControls) - }); + const userInputControls = userInputs.map(input => this.createUserInputFormControl(input)); + this.userInputsFormArray = this.fb.array(userInputControls); } - private createUserInputFormControl(userInput: UserFilterInputInfo): FormGroup { + private createUserInputFormControl(userInput: UserFilterInputInfo): FormGroup { const predicateValue: FilterPredicateValue = (userInput.info.keyFilterPredicate as any).value; - let value = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue; + let value: string | number | boolean = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue; let unitSymbol = ''; let valueConvertor: TbUnitConverter; if (userInput.valueType === EntityKeyValueType.NUMERIC) { @@ -91,42 +86,31 @@ export class UserFilterDialogComponent extends DialogComponent({ + label: this.fb.control(userInput.label), + valueType: this.fb.control(userInput.valueType), + unitSymbol: this.fb.control(unitSymbol), + value: this.fb.control(value, userInput.valueType === EntityKeyValueType.NUMERIC || - userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : []] + userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : []) }); - userInputControl.get('value').valueChanges.pipe( + userInputControl.controls.value.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(userValue => { - let value = userValue; + let val = userValue; if (valueConvertor) { - value = valueConvertor(value as number); + val = valueConvertor(val as number); } - (userInput.info.keyFilterPredicate as any).value.userValue = value; + (userInput.info.keyFilterPredicate as any).value.userValue = val; }); return userInputControl; } - userInputsFormArray(): FormArray { - return this.userFilterFormGroup.get('userInputs') as FormArray; - } - - isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { - const originalErrorState = this.errorStateMatcher.isErrorState(control, form); - const customErrorState = !!(control && control.invalid && this.submitted); - return originalErrorState || customErrorState; - } - cancel(): void { this.dialogRef.close(null); } save(): void { - this.submitted = true; this.dialogRef.close(this.filter); } } diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.ts b/ui-ngx/src/app/shared/components/time/datetime.component.ts index 6d89185702..8306b14872 100644 --- a/ui-ngx/src/app/shared/components/time/datetime.component.ts +++ b/ui-ngx/src/app/shared/components/time/datetime.component.ts @@ -40,7 +40,7 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor { return this.requiredValue; } @Input() - set required(value: boolean) { + set required(value: unknown) { this.requiredValue = coerceBooleanProperty(value); } diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 258f09a535..cd35c410dc 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -411,8 +411,7 @@ export interface FiltersInfo { export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array, operation: ComplexOperation = ComplexOperation.AND): string { const filtersText = keyFilters.map(keyFilter => - keyFilterToText(translate, datePipe, keyFilter, - keyFilters.length > 1 ? operation : undefined)); + keyFilterToText(translate, datePipe, keyFilter, operation)); let result: string; if (filtersText.length > 1) { const opText = translate.instant(complexOperationTranslationMap.get(operation)); From 584214ebff3b5ef8a31da8a69a14ad3078e76aa5 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 31 Mar 2026 18:49:07 +0300 Subject: [PATCH 214/313] Optimize REST API call node request body template UI Consolidate body template section: use outline mat-form-field with always-floating label, merge variable syntax and empty-field hints into a single mat-hint, and remove redundant translation key. --- .../rest-api-call-config.component.html | 26 +++++++++---------- .../rest-api-call-config.component.scss | 3 +++ .../assets/locale/locale.constant-en_US.json | 3 +-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html index 948089721c..0bb2b708f9 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html @@ -124,24 +124,22 @@
-
+
{{ 'rule-node-config.ignore-request-body' | translate }} + @if(!restApiCallConfigForm.get('ignoreRequestBody').value) { + + rule-node-config.request-body-template + + + + }
-
- -
- - rule-node-config.request-body-template - - rule-node-config.request-body-template-empty-hint - -
rule-node-config.read-timeout diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss index a5ed420c88..a4ad3fe4f8 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.scss @@ -26,4 +26,7 @@ } } } + textarea.tb-enable-vertical-resize { + resize: vertical; + } } 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 0dddc1a32e..deeb3117be 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5512,8 +5512,7 @@ "request-method": "Request method", "ignore-request-body": "Without request body", "request-body-template": "Request body template", - "request-body-template-hint": "Use ${metadataKey} for value from metadata, $[messageKey] for value from message body", - "request-body-template-empty-hint": "If empty, the incoming message payload is used as the request body. Supports any content type — use 'Parse to plain text' option to send non-JSON content", + "request-body-template-hint": "Use ${metadataKey} for metadata, $[messageKey] for message values. If empty, the message payload is used as the request body. For non-JSON payloads, enable 'Parse to plain text'.", "parse-to-plain-text": "Parse to plain text", "parse-to-plain-text-hint": "If selected, the request body (message payload or body template result) will be sent as a plain text string instead of being parsed as JSON, e.g. msg = \"Hello,\\t\"world\"\" will be parsed to Hello, \"world\"", "read-timeout": "Read timeout in millis", From db1d2597093efd2bcfa5ff2722bb27cdee6f5916 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 1 Apr 2026 11:07:41 +0300 Subject: [PATCH 215/313] Fix TbRestApiCallNodeTest --- .../engine/rest/TbRestApiCallNodeTest.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index fbe4aa4136..5299f9160a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -55,6 +55,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -69,8 +70,6 @@ import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { - private RuleNode ruleNode; - @Spy private TbRestApiCallNode restNode; @@ -106,7 +105,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { @BeforeEach public void setup() { - ruleNode = new RuleNode(); + RuleNode ruleNode = new RuleNode(); ruleNode.setId(ruleNodeId); ruleNode.setName("Test REST API call node"); lenient().when(ctx.getSelf()).thenReturn(ruleNode); @@ -315,7 +314,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { public void postRequestWithBodyTemplate() throws IOException, InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final String path = "/api/token"; - final String[] capturedBody = new String[1]; + final AtomicReference capturedBody = new AtomicReference<>(); setupServerWithBodyCapture(capturedBody, latch); TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); @@ -337,14 +336,14 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { restNode.onMsg(ctx, msg); assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); - assertEquals("{\"grant_type\":\"client_credentials\",\"client_id\":\"my-client-123\",\"value\":\"abc-xyz\"}", capturedBody[0]); + assertEquals("{\"grant_type\":\"client_credentials\",\"client_id\":\"my-client-123\",\"value\":\"abc-xyz\"}", capturedBody.get()); } @Test public void postRequestWithBodyTemplateAndParseToPlainText() throws IOException, InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final String path = "/api/text"; - final String[] capturedBody = new String[1]; + final AtomicReference capturedBody = new AtomicReference<>(); setupServerWithBodyCapture(capturedBody, latch); TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); @@ -367,14 +366,14 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { restNode.onMsg(ctx, msg); assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); - assertEquals("Hello World, your token is abc-xyz!", capturedBody[0]); + assertEquals("Hello World, your token is abc-xyz!", capturedBody.get()); } @Test public void postRequestWithBodyTemplateEscapesJsonSpecialChars() throws IOException, InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final String path = "/api/token"; - final String[] capturedBody = new String[1]; + final AtomicReference capturedBody = new AtomicReference<>(); setupServerWithBodyCapture(capturedBody, latch); TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); @@ -396,14 +395,14 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { restNode.onMsg(ctx, msg); assertTrue(latch.await(10, TimeUnit.SECONDS), "Server handled request"); - assertEquals("{\"name\":\"John \\\"Doe\\\"\",\"desc\":\"line1\\nline2\"}", capturedBody[0]); + assertEquals("{\"name\":\"John \\\"Doe\\\"\",\"desc\":\"line1\\nline2\"}", capturedBody.get()); } @Test public void postRequestWithEmptyBodyTemplateUsesMessageData() throws IOException, InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final String path = "/api/data"; - final String[] capturedBody = new String[1]; + final AtomicReference capturedBody = new AtomicReference<>(); setupServerWithBodyCapture(capturedBody, latch); TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); @@ -429,15 +428,15 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest { ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx, timeout(10_000)).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("{\"temperature\":25}", capturedBody[0]); + assertEquals("{\"temperature\":25}", capturedBody.get()); } - private void setupServerWithBodyCapture(String[] capturedBody, CountDownLatch latch) throws IOException { + private void setupServerWithBodyCapture(AtomicReference capturedBody, CountDownLatch latch) throws IOException { setupServer("*", (request, response, _) -> { try { if (request instanceof org.apache.http.HttpEntityEnclosingRequest entityRequest) { InputStream is = entityRequest.getEntity().getContent(); - capturedBody[0] = new String(is.readAllBytes(), StandardCharsets.UTF_8); + capturedBody.set(new String(is.readAllBytes(), StandardCharsets.UTF_8)); } response.setStatusCode(200); latch.countDown(); From 253f70d7895f61bb1a2278be8efa1ae345841cab Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 1 Apr 2026 11:10:29 +0300 Subject: [PATCH 216/313] Refactor alarm rule controller to remove cross-controller dependency Move test script execution logic from controllers to TbCalculatedFieldService, eliminating the dependency of AlarmRuleController on CalculatedFieldController. Simplify entity type filtering in getAlarmRules. Add AlarmRuleControllerTest covering all endpoints. --- .../controller/AlarmRuleController.java | 86 +--- .../controller/CalculatedFieldController.java | 91 +--- .../cf/DefaultTbCalculatedFieldService.java | 92 ++++ .../entitiy/cf/TbCalculatedFieldService.java | 3 + .../controller/AlarmRuleControllerTest.java | 420 ++++++++++++++++++ 5 files changed, 533 insertions(+), 159 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java index d2e9adf3ea..165b1ddcfa 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java @@ -15,15 +15,10 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -35,11 +30,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.script.api.tbel.TbelCfCtx; -import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; -import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; @@ -49,6 +39,7 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -60,23 +51,17 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; -import java.util.ArrayList; -import java.util.Collections; import java.util.EnumSet; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.server.controller.CalculatedFieldController.TIMEOUT; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; @@ -93,13 +78,10 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @TbCoreComponent @RequestMapping("/api") @RequiredArgsConstructor -@Slf4j public class AlarmRuleController extends BaseController { private final TbCalculatedFieldService tbCalculatedFieldService; - private final CalculatedFieldController calculatedFieldController; private final EventService eventService; - private final TbelInvokeService tbelInvokeService; public static final String ALARM_RULE_ID = "alarmRuleId"; @@ -130,7 +112,7 @@ public class AlarmRuleController extends BaseController { alarmRuleDefinition.setTenantId(getTenantId()); checkEntityId(alarmRuleDefinition.getEntityId(), Operation.WRITE_CALCULATED_FIELD); CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField(); - calculatedFieldController.checkReferencedEntities(calculatedField.getConfiguration()); + checkReferencedEntities(calculatedField.getConfiguration()); CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser()); return AlarmRuleDefinition.fromCalculatedField(saved); } @@ -188,12 +170,10 @@ public class AlarmRuleController extends BaseController { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); SecurityUser user = getCurrentUser(); - Set types = EnumSet.of(CalculatedFieldType.ALARM); - Set entityTypes; if (entityType == null) { entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream() - .filter(entry -> CollectionUtils.containsAny(entry.getValue(), types)) + .filter(entry -> entry.getValue().contains(CalculatedFieldType.ALARM)) .map(Map.Entry::getKey) .collect(Collectors.toSet()); } else { @@ -201,7 +181,7 @@ public class AlarmRuleController extends BaseController { } CalculatedFieldFilter filter = CalculatedFieldFilter.builder() - .types(types) + .types(EnumSet.of(CalculatedFieldType.ALARM)) .entityTypes(entityTypes) .entityIds(entities) .build(); @@ -262,57 +242,21 @@ public class AlarmRuleController extends BaseController { @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL condition expression. The expression must return a boolean value.") @RequestBody JsonNode inputParams) throws ThingsboardException { checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null); - String expression = inputParams.get("expression").asText(); - Map arguments = Objects.requireNonNullElse( - JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), - Collections.emptyMap() - ); - - ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); - ctxAndArgNames.add("ctx"); - ctxAndArgNames.addAll(arguments.keySet()); - - String output = ""; - String errorText = ""; - - CalculatedFieldTbelScriptEngine engine = null; - try { - if (tbelInvokeService == null) { - throw new IllegalArgumentException("TBEL script engine is disabled!"); - } - - engine = new CalculatedFieldTbelScriptEngine( - getTenantId(), - tbelInvokeService, - expression, - ctxAndArgNames.toArray(String[]::new) - ); + return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams); + } - Object[] args = new Object[ctxAndArgNames.size()]; - args[0] = new TbelCfCtx(arguments, CalculatedFieldController.getLatestTimestamp(arguments)); - for (int i = 1; i < ctxAndArgNames.size(); i++) { - var arg = arguments.get(ctxAndArgNames.get(i)); - if (arg instanceof TbelCfSingleValueArg svArg) { - args[i] = svArg.getValue(); - } else { - args[i] = arg; + private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { + Set referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); + for (EntityId referencedEntityId : referencedEntityIds) { + EntityType refEntityType = referencedEntityId.getEntityType(); + switch (refEntityType) { + case TENANT -> { + return; } - } - - JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); - output = JacksonUtil.toString(json); - } catch (Exception e) { - log.error("Error evaluating expression", e); - Throwable rootCause = ExceptionUtils.getRootCause(e); - errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); - } finally { - if (engine != null) { - engine.destroy(); + case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); + default -> throw new IllegalArgumentException("Unsupported referenced entity type: '" + refEntityType + "'."); } } - return JacksonUtil.newObjectNode() - .put("output", output) - .put("error", errorText); } private CalculatedField checkAlarmRule(CalculatedFieldId calculatedFieldId) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index cd69c717e8..2252ebae2d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; @@ -24,10 +23,7 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn; 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.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.MultiValueMap; @@ -40,13 +36,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.script.api.tbel.TbelCfCtx; -import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; -import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; -import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; -import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.cf.CalculatedField; @@ -65,21 +54,15 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; -import java.util.ArrayList; -import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; -import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; @@ -98,17 +81,13 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @TbCoreComponent @RequestMapping("/api") @RequiredArgsConstructor -@Slf4j public class CalculatedFieldController extends BaseController { private final TbCalculatedFieldService tbCalculatedFieldService; private final EventService eventService; - private final TbelInvokeService tbelInvokeService; public static final String CALCULATED_FIELD_ID = "calculatedFieldId"; - static final int TIMEOUT = 20; - static final String TEST_SCRIPT_EXPRESSION = "Execute the Script expression and return the result. The format of request: \n\n" + MARKDOWN_CODE_BLOCK_START @@ -305,75 +284,11 @@ public class CalculatedFieldController extends BaseController { @PostMapping("/calculatedField/testScript") public JsonNode testCalculatedFieldScript( @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.") - @RequestBody JsonNode inputParams) { - String expression = inputParams.get("expression").asText(); - Map arguments = Objects.requireNonNullElse( - JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), - Collections.emptyMap() - ); - - ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); - ctxAndArgNames.add("ctx"); - ctxAndArgNames.addAll(arguments.keySet()); - - String output = ""; - String errorText = ""; - - CalculatedFieldTbelScriptEngine engine = null; - try { - if (tbelInvokeService == null) { - throw new IllegalArgumentException("TBEL script engine is disabled!"); - } - - engine = new CalculatedFieldTbelScriptEngine( - getTenantId(), - tbelInvokeService, - expression, - ctxAndArgNames.toArray(String[]::new) - ); - - Object[] args = new Object[ctxAndArgNames.size()]; - args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); - for (int i = 1; i < ctxAndArgNames.size(); i++) { - var arg = arguments.get(ctxAndArgNames.get(i)); - if (arg instanceof TbelCfSingleValueArg svArg) { - args[i] = svArg.getValue(); - } else { - args[i] = arg; - } - } - - JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); - output = JacksonUtil.toString(json); - } catch (Exception e) { - log.error("Error evaluating expression", e); - Throwable rootCause = ExceptionUtils.getRootCause(e); - errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); - } finally { - if (engine != null) { - engine.destroy(); - } - } - return JacksonUtil.newObjectNode() - .put("output", output) - .put("error", errorText); - } - - static long getLatestTimestamp(Map arguments) { - long lastUpdateTimestamp = -1; - for (TbelCfArg entry : arguments.values()) { - if (entry instanceof TbelCfSingleValueArg singleValueArg) { - long ts = singleValueArg.getTs(); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); - } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { - long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); - } - } - return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; + @RequestBody JsonNode inputParams) throws ThingsboardException { + return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams); } - void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { + private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { Set referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); for (EntityId referencedEntityId : referencedEntityIds) { EntityType refEntityType = referencedEntityId.getEntityType(); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java index f175c46706..a03b828ddb 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java @@ -15,10 +15,22 @@ */ package org.thingsboard.server.service.entitiy.cf; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; +import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; +import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; @@ -31,10 +43,16 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.TimeUnit; @TbCoreComponent @Service @@ -42,8 +60,13 @@ import java.util.Set; @RequiredArgsConstructor public class DefaultTbCalculatedFieldService extends AbstractTbEntityService implements TbCalculatedFieldService { + private static final int TIMEOUT = 20; + private final CalculatedFieldService calculatedFieldService; + @Autowired(required = false) + private TbelInvokeService tbelInvokeService; + @Override public CalculatedField save(CalculatedField calculatedField, SecurityUser user) throws ThingsboardException { ActionType actionType = calculatedField.getId() == null ? ActionType.ADDED : ActionType.UPDATED; @@ -89,6 +112,75 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp } } + @Override + public JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams) { + String expression = inputParams.get("expression").asText(); + Map arguments = Objects.requireNonNullElse( + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), + Collections.emptyMap() + ); + + ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); + ctxAndArgNames.add("ctx"); + ctxAndArgNames.addAll(arguments.keySet()); + + String output = ""; + String errorText = ""; + + CalculatedFieldTbelScriptEngine engine = null; + try { + if (tbelInvokeService == null) { + throw new IllegalArgumentException("TBEL script engine is disabled!"); + } + + engine = new CalculatedFieldTbelScriptEngine( + tenantId, + tbelInvokeService, + expression, + ctxAndArgNames.toArray(String[]::new) + ); + + Object[] args = new Object[ctxAndArgNames.size()]; + args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); + for (int i = 1; i < ctxAndArgNames.size(); i++) { + var arg = arguments.get(ctxAndArgNames.get(i)); + if (arg instanceof TbelCfSingleValueArg svArg) { + args[i] = svArg.getValue(); + } else { + args[i] = arg; + } + } + + JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); + output = JacksonUtil.toString(json); + } catch (Exception e) { + log.error("Error evaluating expression", e); + Throwable rootCause = ExceptionUtils.getRootCause(e); + errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); + } finally { + if (engine != null) { + engine.destroy(); + } + } + return JacksonUtil.newObjectNode() + .put("output", output) + .put("error", errorText); + } + + private static long getLatestTimestamp(Map arguments) { + long lastUpdateTimestamp = -1; + for (TbelCfArg entry : arguments.values()) { + if (entry instanceof TbelCfSingleValueArg singleValueArg) { + long ts = singleValueArg.getTs(); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); + } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { + long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); + } + } + return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; + } + private void checkForEntityChange(CalculatedField oldCalculatedField, CalculatedField newCalculatedField) { if (!oldCalculatedField.getEntityId().equals(newCalculatedField.getEntityId())) { throw new IllegalArgumentException("Changing the calculated field target entity after initialization is prohibited."); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java index 5fbb96e636..9dbb20dc2f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy.cf; +import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -35,4 +36,6 @@ public interface TbCalculatedFieldService { void delete(CalculatedField calculatedField, SecurityUser user); + JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams); + } diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java new file mode 100644 index 0000000000..f8bbd963ba --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java @@ -0,0 +1,420 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +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.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class AlarmRuleControllerTest extends AbstractControllerTest { + + private Tenant savedTenant; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = saveTenant(tenant); + assertThat(savedTenant).isNotNull(); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + deleteTenant(savedTenant.getId()); + } + + @Test + public void testSaveAlarmRule() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "High Temperature"); + + AlarmRuleDefinition saved = saveAlarmRule(alarmRule); + + assertThat(saved).isNotNull(); + assertThat(saved.getId()).isNotNull(); + assertThat(saved.getCreatedTime()).isGreaterThan(0); + assertThat(saved.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(saved.getEntityId()).isEqualTo(testDevice.getId()); + assertThat(saved.getName()).isEqualTo("High Temperature"); + assertThat(saved.getConfiguration()).isNotNull(); + assertThat(saved.getConfiguration().getCreateRules()).containsKey(AlarmSeverity.CRITICAL); + + saved.setName("Updated Alarm Rule"); + AlarmRuleDefinition updated = saveAlarmRule(saved); + + assertThat(updated.getName()).isEqualTo("Updated Alarm Rule"); + assertThat(updated.getVersion()).isEqualTo(saved.getVersion() + 1); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetAlarmRuleById() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "Test Alarm"); + + AlarmRuleDefinition saved = saveAlarmRule(alarmRule); + AlarmRuleDefinition fetched = doGet("/api/alarm/rule/" + saved.getId().getId(), AlarmRuleDefinition.class); + + assertThat(fetched).isNotNull(); + assertThat(fetched).isEqualTo(saved); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetAlarmRuleById_notFound() throws Exception { + doGet("/api/alarm/rule/" + UUID.randomUUID()) + .andExpect(status().isNotFound()); + } + + @Test + public void testGetAlarmRuleById_calculatedFieldNotAlarm() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField cf = createSimpleCalculatedField(testDevice.getId()); + CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class); + + doGet("/api/alarm/rule/" + savedCf.getId().getId()) + .andExpect(status().isNotFound()); + + doDelete("/api/calculatedField/" + savedCf.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetAlarmRulesByEntityId() throws Exception { + Device device1 = createDevice("Device 1", "1234567890"); + Device device2 = createDevice("Device 2", "0987654321"); + AlarmRuleDefinition rule1 = saveAlarmRule(createTestAlarmRule(device1.getId(), "Rule 1")); + saveAlarmRule(createTestAlarmRule(device2.getId(), "Rule 2")); + + PageData result = doGetTypedWithPageLink( + "/api/alarm/rules/" + EntityType.DEVICE + "/" + device1.getUuidId() + "?", + new TypeReference<>() {}, new PageLink(10)); + + assertThat(result.getData()).hasSize(1); + assertThat(result.getData().get(0).getId()).isEqualTo(rule1.getId()); + assertThat(result.getData().get(0).getName()).isEqualTo("Rule 1"); + } + + @Test + public void testGetAlarmRules() throws Exception { + Device device = createDevice("Device A", "1234567890"); + AlarmRuleDefinition deviceRule = saveAlarmRule(createTestAlarmRule(device.getId(), "Device Alarm")); + + DeviceProfile profile = doPost("/api/deviceProfile", createDeviceProfile("Profile A"), DeviceProfile.class); + AlarmRuleDefinition profileRule = saveAlarmRule(createTestAlarmRule(profile.getId(), "Profile Alarm")); + + // All alarm rules + List all = getAlarmRules(null, null); + assertThat(all).extracting(AlarmRuleDefinition::getName) + .contains("Device Alarm", "Profile Alarm"); + + // Filter by entity type: DEVICE + List deviceRules = getAlarmRules(EntityType.DEVICE, null); + assertThat(deviceRules).extracting(AlarmRuleDefinition::getName) + .containsOnly("Device Alarm"); + + // Filter by entity type: DEVICE_PROFILE + List profileRules = getAlarmRules(EntityType.DEVICE_PROFILE, null); + assertThat(profileRules).extracting(AlarmRuleDefinition::getName) + .containsOnly("Profile Alarm"); + + // Filter by specific entity IDs + List specificRules = getAlarmRules(EntityType.DEVICE, List.of(device.getUuidId())); + assertThat(specificRules).extracting(AlarmRuleDefinition::getName) + .containsOnly("Device Alarm"); + + // Verify entity names are populated + AlarmRuleDefinitionInfo deviceInfo = all.stream() + .filter(r -> r.getName().equals("Device Alarm")).findFirst().orElseThrow(); + assertThat(deviceInfo.getEntityName()).isEqualTo("Device A"); + + AlarmRuleDefinitionInfo profileInfo = all.stream() + .filter(r -> r.getName().equals("Profile Alarm")).findFirst().orElseThrow(); + assertThat(profileInfo.getEntityName()).isEqualTo("Profile A"); + } + + @Test + public void testGetAlarmRules_textSearch() throws Exception { + Device device = createDevice("Device A", "1234567890"); + saveAlarmRule(createTestAlarmRule(device.getId(), "Temperature Alarm")); + saveAlarmRule(createTestAlarmRule(device.getId(), "Humidity Alarm")); + + PageData result = doGetTypedWithPageLink( + "/api/alarm/rules?textSearch=Temp&", + new TypeReference<>() {}, new PageLink(10)); + + assertThat(result.getData()).hasSize(1); + assertThat(result.getData().get(0).getName()).isEqualTo("Temperature Alarm"); + } + + @Test + public void testGetAlarmRuleNames() throws Exception { + Device device = createDevice("Device A", "1234567890"); + saveAlarmRule(createTestAlarmRule(device.getId(), "Alpha Alarm")); + saveAlarmRule(createTestAlarmRule(device.getId(), "Beta Alarm")); + + PageData names = getAlarmRuleNames(new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.ASC))); + assertThat(names.getTotalElements()).isEqualTo(2); + assertThat(names.getData()).isSortedAccordingTo(Comparator.naturalOrder()); + assertThat(names.getData()).contains("Alpha Alarm", "Beta Alarm"); + + names = getAlarmRuleNames(new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.DESC))); + assertThat(names.getData()).isSortedAccordingTo(Comparator.reverseOrder()); + + names = getAlarmRuleNames(new PageLink(10, 0, + "Alpha", new SortOrder("", SortOrder.Direction.ASC))); + assertThat(names.getTotalElements()).isEqualTo(1); + assertThat(names.getData()).containsOnly("Alpha Alarm"); + } + + @Test + public void testDeleteAlarmRule() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "To Delete")); + + assertThat(saved).isNotNull(); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + doGet("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isNotFound()); + } + + @Test + public void testDeleteAlarmRule_notFound() throws Exception { + doDelete("/api/alarm/rule/" + UUID.randomUUID()) + .andExpect(status().isNotFound()); + } + + @Test + public void testDeleteAlarmRule_calculatedFieldNotAlarm() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField cf = createSimpleCalculatedField(testDevice.getId()); + CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class); + + doDelete("/api/alarm/rule/" + savedCf.getId().getId()) + .andExpect(status().isNotFound()); + + doDelete("/api/calculatedField/" + savedCf.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetLatestAlarmRuleDebugEvent() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "Debug Test")); + + JsonNode result = doGet("/api/alarm/rule/" + saved.getId().getId() + "/debug", JsonNode.class); + assertThat(result).isNull(); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetLatestAlarmRuleDebugEvent_notFound() throws Exception { + doGet("/api/alarm/rule/" + UUID.randomUUID() + "/debug") + .andExpect(status().isNotFound()); + } + + @Test + public void testTestAlarmRuleScript() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "expression": "return temperature > 50;", + "arguments": { + "temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 55 } + } + } + """); + + JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); + + assertThat(result).isNotNull(); + assertThat(result.has("output")).isTrue(); + assertThat(result.has("error")).isTrue(); + assertThat(result.get("error").asText()).isEmpty(); + assertThat(result.get("output").asText()).isEqualTo("true"); + } + + @Test + public void testTestAlarmRuleScript_returnsFalse() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "expression": "return temperature > 50;", + "arguments": { + "temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 30 } + } + } + """); + + JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); + + assertThat(result).isNotNull(); + assertThat(result.get("error").asText()).isEmpty(); + assertThat(result.get("output").asText()).isEqualTo("false"); + } + + @Test + public void testTestAlarmRuleScript_missingExpression() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "arguments": {} + } + """); + + doPost("/api/alarm/rule/testScript", request) + .andExpect(status().isBadRequest()); + } + + @Test + public void testTestAlarmRuleScript_invalidExpression() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "expression": "invalid syntax {{{{", + "arguments": {} + } + """); + + JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); + + assertThat(result).isNotNull(); + assertThat(result.get("error").asText()).isNotEmpty(); + } + + // --- Helper methods --- + + private AlarmRuleDefinition createTestAlarmRule(EntityId entityId, String name) { + AlarmRuleDefinition alarmRule = new AlarmRuleDefinition(); + alarmRule.setEntityId(entityId); + alarmRule.setName(name); + alarmRule.setConfigurationVersion(1); + alarmRule.setAdditionalInfo(JacksonUtil.newObjectNode()); + + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("0"); + configuration.setArguments(Map.of("temperature", argument)); + + AlarmRule rule = new AlarmRule(); + TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); + expression.setExpression("return temperature >= 50;"); + SimpleAlarmCondition condition = new SimpleAlarmCondition(); + condition.setExpression(expression); + rule.setCondition(condition); + configuration.setCreateRules(Map.of(AlarmSeverity.CRITICAL, rule)); + + alarmRule.setConfiguration(configuration); + return alarmRule; + } + + private CalculatedField createSimpleCalculatedField(EntityId entityId) { + CalculatedField cf = new CalculatedField(); + cf.setEntityId(entityId); + cf.setType(CalculatedFieldType.SIMPLE); + cf.setName("Simple CF"); + cf.setConfigurationVersion(1); + cf.setAdditionalInfo(JacksonUtil.newObjectNode()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + config.setArguments(Map.of("T", arg)); + config.setExpression("T * 2"); + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("result"); + config.setOutput(output); + cf.setConfiguration(config); + + return cf; + } + + private List getAlarmRules(EntityType entityType, List entities) throws Exception { + StringBuilder url = new StringBuilder("/api/alarm/rules?"); + if (entityType != null) { + url.append("entityType=").append(entityType).append("&"); + } + if (entities != null) { + url.append("entities=").append(String.join(",", + entities.stream().map(UUID::toString).toList())).append("&"); + } + return doGetTypedWithPageLink(url.toString(), + new TypeReference>() {}, new PageLink(10)).getData(); + } + + private PageData getAlarmRuleNames(PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/alarm/rules/names?", + new TypeReference>() {}, pageLink); + } + +} From 1dc1b35ef155748a102b1bff6ad797b9822c9159 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 1 Apr 2026 11:22:51 +0300 Subject: [PATCH 217/313] Use specific CalculatedFieldAlarmRule types in alarm rules UI components --- .../src/app/core/http/alarm-rules.service.ts | 20 +++++------ .../alarm-rule-dialog.component.ts | 18 ++++++---- .../alarm-rules/alarm-rules-table-config.ts | 23 ++++++------ .../alarm-rules/alarm-rules.component.ts | 35 ++++++++----------- .../pages/alarm/alarm-rules-tabs.component.ts | 13 ++++--- .../shared/models/calculated-field.models.ts | 6 ++-- 6 files changed, 56 insertions(+), 59 deletions(-) diff --git a/ui-ngx/src/app/core/http/alarm-rules.service.ts b/ui-ngx/src/app/core/http/alarm-rules.service.ts index 2ab7857793..18a51115f3 100644 --- a/ui-ngx/src/app/core/http/alarm-rules.service.ts +++ b/ui-ngx/src/app/core/http/alarm-rules.service.ts @@ -20,8 +20,8 @@ import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; import { - CalculatedField, - CalculatedFieldInfo, + CalculatedFieldAlarmRule, + CalculatedFieldAlarmRuleInfo, CalculatedFieldsQuery, CalculatedFieldTestScriptInputParams } from '@shared/models/calculated-field.models'; @@ -39,24 +39,24 @@ export class AlarmRulesService { private http: HttpClient ) { } - public getAlarmRuleById(alarmRuleId: string, config?: RequestConfig): Observable { - return this.http.get(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + public getAlarmRuleById(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); } - public saveAlarmRule(alarmRule: CalculatedField, config?: RequestConfig): Observable { - return this.http.post('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config)); + public saveAlarmRule(alarmRule: CalculatedFieldAlarmRule, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config)); } public deleteAlarmRule(alarmRuleId: string, config?: RequestConfig): Observable { return this.http.delete(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); } - public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { - return this.http.get>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); } - public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/alarm/rules/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { 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 97cdd91689..10793723db 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 @@ -21,7 +21,11 @@ import { AppState } from '@core/core.state'; import { FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; -import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models'; +import { + CalculatedFieldAlarmRule, + CalculatedFieldArgument, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; @@ -49,13 +53,13 @@ import { AssetInfo } from '@shared/models/asset.models'; import { DeviceInfo } from '@shared/models/device.models'; export interface AlarmRuleDialogData { - value?: CalculatedField; + value?: CalculatedFieldAlarmRule; buttonTitle: string; entityId: EntityId; tenantId: string; entityName?: string; ownerId: EntityId; - additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; + additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedFieldAlarmRule) => void>; isDirty?: boolean; getTestScriptDialogFn: AlarmRuleTestScriptFn, } @@ -67,7 +71,7 @@ export interface AlarmRuleDialogData { encapsulation: ViewEncapsulation.None, standalone: false }) -export class AlarmRuleDialogComponent extends DialogComponent { +export class AlarmRuleDialogComponent extends DialogComponent { @ViewChild('entitySelect') entitySelect!: EntitySelectComponent; @@ -96,7 +100,7 @@ export class AlarmRuleDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, - protected dialogRef: MatDialogRef, + protected dialogRef: MatDialogRef, private alarmRulesService: AlarmRulesService, private destroyRef: DestroyRef, private cfFormService: CalculatedFieldFormService) { @@ -159,8 +163,8 @@ export class AlarmRuleDialogComponent extends DialogComponent { @@ -159,9 +158,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig('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%', + 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)); } @@ -260,7 +259,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { + 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, { + const entityName = this.entityName || (value as CalculatedFieldAlarmRuleInfo)?.entityName; + return this.dialog.open(AlarmRuleDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { @@ -360,7 +359,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.updateData()); } - private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField { + private updateImportedCalculatedField(calculatedField: CalculatedFieldAlarmRule): CalculatedFieldAlarmRule { if (calculatedField.type === CalculatedFieldType.ALARM) { calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { const arg = calculatedField.configuration.arguments[key]; @@ -382,7 +381,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.updateData()); } - private getTestScriptDialog(calculatedField: AlarmRuleTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { + getTestScriptDialog(calculatedField: AlarmRuleTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): 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/alarm-rules/alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts index 13c388675c..740dd30440 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts @@ -22,19 +22,19 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { EntityType } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { + CalculatedFieldAlarmRuleConfiguration, + CalculatedFieldAlarmRuleInfo, CalculatedFieldArgument, - CalculatedFieldConfiguration, - CalculatedFieldInfo, CalculatedFieldType } from '@shared/models/calculated-field.models'; import { EntityId } from '@shared/models/id/entity-id'; import { BaseData } from '@shared/models/base-data'; import { Observable } from 'rxjs'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; -import { - CalculatedFieldsTableConfig, - CalculatedFieldsTableEntity -} from '@home/components/calculated-fields/calculated-fields-table-config'; +import type { + AlarmRulesTableConfig, + AlarmRuleTableEntity +} from '@home/components/alarm-rules/alarm-rules-table-config'; import { TenantId } from '@shared/models/id/tenant-id'; import { StringItemsOption } from '@shared/components/string-items-list.component'; import { RelationTypes } from '@shared/models/relation.models'; @@ -56,7 +56,7 @@ import { EntityService } from '@core/http/entity.service'; styleUrls: ['./alarm-rule-dialog.component.scss'], standalone: false }) -export class AlarmRulesComponent extends EntityComponent { +export class AlarmRulesComponent extends EntityComponent { @Input() standalone = false; @@ -75,8 +75,8 @@ export class AlarmRulesComponent extends EntityComponent, protected translate: TranslateService, - @Inject('entity') protected entityValue: CalculatedFieldInfo, - @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig, + @Inject('entity') protected entityValue: CalculatedFieldAlarmRuleInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: AlarmRulesTableConfig, protected fb: FormBuilder, protected cd: ChangeDetectorRef, private entityService: EntityService) { @@ -93,21 +93,14 @@ export class AlarmRulesComponent extends EntityComponent this.entitiesTableConfig.additionalDebugActionConfig.action( - { id: this.entity.id, ...this.entityFormValue() }, false, - (expression) => { - if (expression) { - this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression}); - this.entityForm.get('configuration').markAsDirty(); - } - }), + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action({id: this.entity.id, ...this.entityFormValue()}) }; get entityId(): EntityId { return this.entityForm.get('entityId').value; } - get entitiesTableConfig(): CalculatedFieldsTableConfig { + get entitiesTableConfig(): AlarmRulesTableConfig { return this.entitiesTableConfigValue; } @@ -115,12 +108,12 @@ export class AlarmRulesComponent extends EntityComponent { +export class AlarmRulesTabsComponent extends EntityTabsComponent { readonly DebugEventType = DebugEventType; readonly EventType = EventType; @@ -43,8 +43,7 @@ export class AlarmRulesTabsComponent extends EntityTabsComponent { - }); + (this.entitiesTableConfig as AlarmRulesTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments)) + .subscribe((_expression) => { }); }; } 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 c01083d078..552d2a3a95 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -80,7 +80,9 @@ export type CalculatedField = | CalculatedFieldRelatedEntityAggregation | CalculatedFieldAlarmRule; -export type CalculatedFieldInfo = CalculatedField & {entityName: string}; +export type WithCalculatedFieldInfo = T & {entityName: string}; +export type CalculatedFieldInfo = WithCalculatedFieldInfo; +export type CalculatedFieldAlarmRuleInfo = WithCalculatedFieldInfo; export enum CalculatedFieldType { SIMPLE = 'SIMPLE', @@ -192,7 +194,7 @@ interface BasePropagationConfiguration { output: CalculatedFieldOutput; } -interface CalculatedFieldAlarmRuleConfiguration { +export interface CalculatedFieldAlarmRuleConfiguration { type: CalculatedFieldType.ALARM; arguments: Record; createRules: Record; From 781391a4eb880cc19dc7c65f571eb6e3a8e0deb9 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 1 Apr 2026 12:17:52 +0300 Subject: [PATCH 218/313] Improved request body template UI to match headers panel style --- .../rule-node/external/rest-api-call-config.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html index 0bb2b708f9..bf5f29d399 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html @@ -129,6 +129,7 @@ {{ 'rule-node-config.ignore-request-body' | translate }} @if(!restApiCallConfigForm.get('ignoreRequestBody').value) { +
rule-node-config.request-body-template - }
From ff8152395e47ef87071071d643243b1d0e5df08c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 1 Apr 2026 14:56:12 +0300 Subject: [PATCH 219/313] added missed response schemas --- .../thingsboard/server/controller/DashboardController.java | 4 ++++ 1 file changed, 4 insertions(+) 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 0aa74607be..9f59044356 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -151,6 +151,8 @@ public class DashboardController extends BaseController { @ApiOperation(value = "Get Dashboard (getDashboardById)", notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH ) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = Dashboard.class))) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/{dashboardId}") public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @@ -415,6 +417,8 @@ public class DashboardController extends BaseController { "If 'homeDashboardId' parameter is not set on the User level and the User has authority 'CUSTOMER_USER', check the same parameter for the corresponding Customer. " + "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = HomeDashboard.class))) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/home") public void getHomeDashboard(@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, From 12f0094a64069495fc6d25c8efd7bc6739b9e2c8 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 1 Apr 2026 16:56:14 +0300 Subject: [PATCH 220/313] deleted redundant required mode for some entity fields, added appropriate mediaType for some api request body --- .../server/controller/RpcV1Controller.java | 8 ++++++-- .../server/controller/RpcV2Controller.java | 7 +++++-- .../server/controller/RuleEngineController.java | 14 ++++++++++---- .../server/controller/TelemetryController.java | 15 ++++++++++----- .../thingsboard/server/common/data/Customer.java | 2 +- .../thingsboard/server/common/data/Device.java | 2 +- .../server/common/data/DeviceProfileInfo.java | 7 +++++++ 7 files changed, 40 insertions(+), 15 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java index 08ef1a85c8..5e555cbcbe 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java @@ -16,6 +16,8 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -50,7 +52,8 @@ public class RpcV1Controller extends AbstractRpcController { public DeferredResult handleOneWayDeviceRPCRequestV1( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT); } @@ -62,7 +65,8 @@ public class RpcV1Controller extends AbstractRpcController { public DeferredResult handleTwoWayDeviceRPCRequestV1( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT); } diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index fdcb853343..4366134927 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.FutureCallback; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -127,7 +128,8 @@ public class RpcV2Controller extends AbstractRpcController { public DeferredResult handleOneWayDeviceRPCRequestV2( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } @@ -146,7 +148,8 @@ public class RpcV2Controller extends AbstractRpcController { public DeferredResult handleTwoWayDeviceRPCRequestV2( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java index ec39bb97aa..d63e0f5d19 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java @@ -17,6 +17,8 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.FutureCallback; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -87,7 +89,8 @@ public class RuleEngineController extends BaseController { @RequestMapping(value = "/", method = RequestMethod.POST) @ResponseBody public DeferredResult handleRuleEngineRequestForUser( - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleRuleEngineRequestForEntityWithQueueAndTimeout(null, null, null, defaultResponseTimeout, requestBody); } @@ -106,7 +109,8 @@ public class RuleEngineController extends BaseController { @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, defaultResponseTimeout, requestBody); } @@ -127,7 +131,8 @@ public class RuleEngineController extends BaseController { @PathVariable("entityId") String entityIdStr, @Parameter(description = "Timeout to process the request in milliseconds", required = true) @PathVariable("timeout") int timeout, - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, timeout, requestBody); } @@ -151,7 +156,8 @@ public class RuleEngineController extends BaseController { @PathVariable("queueName") String queueName, @Parameter(description = "Timeout to process the request in milliseconds", required = true) @PathVariable("timeout") int timeout, - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { try { SecurityUser currentUser = getCurrentUser(); diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index dc66e4a7b6..754c9253d9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -386,7 +386,8 @@ public class TelemetryController extends BaseController { @PathVariable("deviceId") String deviceIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -411,7 +412,8 @@ public class TelemetryController extends BaseController { @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -436,7 +438,8 @@ public class TelemetryController extends BaseController { @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -460,7 +463,8 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveTelemetry(getTenantId(), entityId, requestBody, 0L); } @@ -484,7 +488,8 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope, @Parameter(description = "A long value representing TTL (Time to Live) parameter.", required = true) @PathVariable("ttl") Long ttl, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveTelemetry(getTenantId(), entityId, requestBody, ttl); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index 8931e6a270..956cf054dd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -134,7 +134,7 @@ public class Customer extends ContactBased implements HasTenantId, E return super.getPhone(); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email", example = "example@company.com") + @Schema(description = "Email", example = "example@company.com") @Override public String getEmail() { return super.getEmail(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index cc0b413025..c0c61bc74f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -176,7 +176,7 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.label = label; } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Device Profile Id.") + @Schema(description = "JSON object with Device Profile Id. If not provided, the default device profile will be used.") public DeviceProfileId getDeviceProfileId() { return deviceProfileId; } 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 8bf65873a3..e21b8d3cd0 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 @@ -22,6 +22,7 @@ import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.Value; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; @@ -75,4 +76,10 @@ public class DeviceProfileInfo extends EntityInfo { profile.getType(), profile.getTransportType()); } + @Override + @Schema(implementation = DeviceProfileId.class, description = "JSON object with the Device Profile Id.") + public EntityId getId() { + return super.getId(); + } + } From 9933b9eb2c326b725583d8a70ae4894e0566a576 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 1 Apr 2026 18:23:36 +0200 Subject: [PATCH 221/313] Moved part of inputs to Outline appearance --- ...-widget-to-dashboard-dialog.component.html | 1 + .../event/event-filter-panel.component.html | 8 +- .../event/event-table-header.component.html | 2 +- ...tenant-profile-autocomplete.component.html | 2 +- .../tenant-profile-autocomplete.component.ts | 4 + .../profile/tenant-profile.component.html | 6 +- ...enant-profile-configuration.component.html | 96 +++++++++---------- .../rate-limits-list.component.html | 4 +- .../queue/queue-form.component.html | 26 ++--- .../relation/relation-dialog.component.html | 4 +- .../relation/relation-table.component.html | 4 +- ...nced-processing-setting-row.component.html | 2 +- ...vanced-processing-setting-row.component.ts | 4 + ...advanced-processing-setting.component.html | 5 + .../assign-customer-config.component.html | 2 +- .../action/attributes-config.component.html | 7 +- .../action/clear-alarm-config.component.html | 2 +- .../action/create-alarm-config.component.html | 10 +- .../create-relation-config.component.html | 6 +- .../delete-attributes-config.component.html | 6 +- .../delete-relation-config.component.html | 5 +- .../action/device-state-config.component.html | 2 +- .../action/generator-config.component.html | 5 +- .../gps-geo-action-config.component.html | 27 +++--- .../math-function-config.component.html | 14 +-- .../action/msg-count-config.component.html | 4 +- .../action/msg-delay-config.component.html | 5 +- .../push-to-cloud-config.component.html | 4 +- .../action/push-to-edge-config.component.html | 4 +- .../action/rpc-reply-config.component.html | 6 +- .../action/rpc-request-config.component.html | 2 +- ...save-to-custom-table-config.component.html | 4 +- ...-rest-api-call-reply-config.component.html | 4 +- .../action/timeseries-config.component.html | 4 +- .../unassign-customer-config.component.html | 2 +- .../common/credentials-config.component.html | 6 +- ...vice-relations-query-config.component.html | 6 +- .../math-function-autocomplete.component.html | 2 +- .../math-function-autocomplete.component.ts | 4 + .../message-types-config.component.html | 2 +- ...t-message-type-autocomplete.component.html | 4 +- ...put-message-type-autocomplete.component.ts | 5 +- .../relations-query-config.component.html | 4 +- .../common/select-attributes.component.html | 4 + .../calculate-delta-config.component.html | 8 +- .../entity-details-config.component.html | 1 + ...emetry-from-database-config.component.html | 22 ++--- .../external/ai-config.component.html | 2 + .../azure-iot-hub-config.component.html | 14 +-- .../external/kafka-config.component.html | 18 ++-- .../external/lambda-config.component.html | 14 +-- .../external/mqtt-config.component.html | 10 +- .../notification-config.component.html | 2 + .../external/pubsub-config.component.html | 4 +- .../external/rabbit-mq-config.component.html | 22 ++--- .../rest-api-call-config.component.html | 18 ++-- .../external/send-email-config.component.html | 22 ++--- .../external/send-sms-config.component.html | 6 +- .../external/slack-config.component.html | 5 +- .../external/sns-config.component.html | 8 +- .../external/sqs-config.component.html | 12 +-- .../check-message-config.component.html | 2 + .../check-relation-config.component.html | 5 +- .../gps-geo-filter-config.component.html | 22 ++--- .../originator-type-config.component.html | 1 + .../flow/rule-chain-input.component.html | 1 + .../change-originator-config.component.html | 8 +- .../copy-keys-config.component.html | 1 + .../deduplication-config.component.html | 16 ++-- .../delete-keys-config.component.html | 1 + .../node-json-path-config.component.html | 2 +- .../to-email-config.component.html | 16 ++-- ...-sns-provider-configuration.component.html | 6 +- ...-sms-provider-configuration.component.html | 30 +++--- .../sms-provider-configuration.component.html | 2 +- ...-sms-provider-configuration.component.html | 6 +- .../admin/general-settings.component.html | 6 +- .../pages/admin/home-settings.component.html | 1 + .../pages/admin/mail-server.component.html | 43 +++++---- .../oauth2/clients/client.component.html | 70 +++++++------- .../oauth2/domains/domain.component.html | 6 +- .../admin/security-settings.component.html | 34 +++---- .../admin/security-settings.component.scss | 18 ++-- .../pages/admin/sms-provider.component.html | 2 +- .../admin/trendz-settings.component.html | 4 +- .../home/pages/profile/profile.component.html | 12 ++- .../rule-node-details.component.html | 3 +- .../pages/security/security.component.html | 6 +- .../home/pages/tenant/tenant.component.html | 14 +-- .../pages/widget/widget-editor.component.html | 25 ++--- .../node-script-test-dialog.component.html | 3 +- .../entity/entity-list-select.component.html | 2 + .../entity/entity-list-select.component.ts | 4 + .../image/image-dialog.component.html | 2 +- .../material-icon-select.component.html | 2 +- .../material-icon-select.component.ts | 4 + .../message-type-autocomplete.component.html | 2 +- .../message-type-autocomplete.component.ts | 4 + .../rule-chain-select.component.html | 2 +- 99 files changed, 488 insertions(+), 418 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html index 769c553635..4fc7c6a911 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html @@ -36,6 +36,7 @@
dashboard.select-existing diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html index f59cac1d7f..fa9044ed9b 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html @@ -20,7 +20,7 @@ @for (column of showColumns; track column) { @switch (column.key) { @case (isSelector(column.key)) { - + {{ column.title | translate}} {{ 'event.all-events' | translate}} @@ -33,7 +33,7 @@ } @case (isNumberFields(column.key)) { - + {{ column.title | translate}} @if (eventFilterFormGroup.get(column.key).hasError('min')) { @@ -50,13 +50,13 @@ } @case ('errorStr') { - + {{ column.title | translate}} } @default { - + {{ column.title | translate}} diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-header.component.html b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.html index d78c466397..df31150835 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-header.component.html +++ b/ui-ngx/src/app/modules/home/components/event/event-table-header.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + event.event-type diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html index 7b639f134c..4d7b127aa3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + tenant-profile.tenant-profile (); diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index ca553da0b2..3a53b4cbc9 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -50,7 +50,7 @@
- + tenant-profile.name @if (entityForm.get('name').hasError('required')) { @@ -103,9 +103,9 @@
- + tenant-profile.description - + diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 7100fce348..ddd159388a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -21,7 +21,7 @@ {{ 'tenant-profile.entities' | translate }} tenant-profile.unlimited
- + tenant-profile.maximum-devices - + tenant-profile.maximum-dashboards
- + tenant-profile.maximum-assets - + tenant-profile.maximum-users
- + tenant-profile.maximum-customers - + tenant-profile.maximum-rule-chains
- + tenant-profile.maximum-edges tenant-profile.unlimited
- + tenant-profile.max-r-e-executions - + tenant-profile.max-transport-messages
- + tenant-profile.max-j-s-executions - + tenant-profile.max-tbel-executions
- + tenant-profile.max-rule-node-executions-per-message - + tenant-profile.max-transport-data-points tenant-profile.unlimited
- + tenant-profile.max-calculated-fields - + tenant-profile.max-data-points-per-rolling-arg
- + tenant-profile.max-arguments-per-cf
- + tenant-profile.max-state-size - + tenant-profile.max-value-argument-size
- + tenant-profile.max-related-level-per-argument - + tenant-profile.min-allowed-scheduled-update-interval
- + tenant-profile.min-allowed-aggregation-interval - + tenant-profile.min-allowed-deduplication-interval
- + tenant-profile.intermediate-aggregation-interval - + tenant-profile.reevaluation-check-interval
- + tenant-profile.relation-search-entity-limit tenant-profile.unlimited
- + tenant-profile.max-d-p-storage-days - + tenant-profile.alarms-ttl-days
- + tenant-profile.default-storage-ttl-days - + tenant-profile.rpc-ttl-days
- + tenant-profile.queue-stats-ttl-days - + tenant-profile.rule-engine-exceptions-ttl-days @if (tenantProfileConfigurationForm.get('smsEnabled').value) { + appearance="outline" subscriptSizing="dynamic"> tenant-profile.max-sms }
- + tenant-profile.max-emails - + tenant-profile.max-created-alarms
- + tenant-profile.alarms-reevaluation-interval
- + tenant-profile.maximum-debug-duration-min tenant-profile.unlimited
- + tenant-profile.maximum-resources-sum-data-size - + tenant-profile.maximum-ota-packages-sum-data-size
- + tenant-profile.maximum-resource-size tenant-profile.unlimited
- + tenant-profile.ws-limit-max-sessions-per-tenant @if (tenantProfileConfigurationForm.get('maxWsSessionsPerTenant').hasError('min')) { @@ -811,7 +811,7 @@ } - + tenant-profile.ws-limit-max-subscriptions-per-tenant @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerTenant').hasError('min')) { @@ -822,7 +822,7 @@
- + tenant-profile.ws-limit-max-sessions-per-customer @if (tenantProfileConfigurationForm.get('maxWsSessionsPerCustomer').hasError('min')) { @@ -831,7 +831,7 @@ } - + tenant-profile.ws-limit-max-subscriptions-per-customer @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerCustomer').hasError('min')) { @@ -849,7 +849,7 @@
- + tenant-profile.ws-limit-max-sessions-per-public-user @if (tenantProfileConfigurationForm.get('maxWsSessionsPerPublicUser').hasError('min')) { @@ -858,7 +858,7 @@ } - + tenant-profile.ws-limit-max-subscriptions-per-public-user @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerPublicUser').hasError('min')) { @@ -869,7 +869,7 @@
- + tenant-profile.ws-limit-max-sessions-per-regular-user @if (tenantProfileConfigurationForm.get('maxWsSessionsPerRegularUser').hasError('min')) { @@ -878,7 +878,7 @@ } - + tenant-profile.ws-limit-max-subscriptions-per-regular-user @if (tenantProfileConfigurationForm.get('maxWsSubscriptionsPerRegularUser').hasError('min')) { @@ -889,7 +889,7 @@
- + tenant-profile.ws-limit-queue-per-session @if (tenantProfileConfigurationForm.get('wsMsgQueueLimitPerSession').hasError('min')) { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html index d24cc15a4b..159dcc6540 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html @@ -22,7 +22,7 @@
tenant-profile.rate-limits.and-also-less-than
}
- + tenant-profile.rate-limits.number-of-messages @@ -33,7 +33,7 @@ tenant-profile.rate-limits.number-of-messages-min } - + tenant-profile.rate-limits.per-seconds diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html index de7b1de846..94803a8739 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html @@ -16,7 +16,7 @@ -->
- + admin.queue-name @if (queueFormGroup.get('name').hasError('required')) { @@ -62,7 +62,7 @@ @if (hideBatchSize) {
- + queue.batch-size @if (queueFormGroup.get('submitStrategy.batchSize').hasError('required')) { @@ -107,7 +107,7 @@
- + queue.retries @@ -123,7 +123,7 @@ } - + queue.failure-percentage @@ -147,7 +147,7 @@ } - + queue.pause-between-retries @@ -163,7 +163,7 @@ } - + queue.max-pause-between-retries @@ -194,7 +194,7 @@
- + queue.poll-interval @if (queueFormGroup.get('pollInterval').hasError('required')) { @@ -209,7 +209,7 @@ } - + queue.partitions @if (queueFormGroup.get('partitions').hasError('required')) { @@ -232,7 +232,7 @@ {{ 'queue.consumer-per-partition' | translate }} - + queue.processing-timeout @if (queueFormGroup.get('packProcessingTimeout').hasError('required')) { @@ -257,14 +257,14 @@ {{ 'queue.duplicate-msg-to-all-partitions' | translate }} - + queue.custom-properties - + queue.custom-properties-hint - + queue.description - + queue.description-hint
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html index 896adb0c46..ffbfb48cf3 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html @@ -34,14 +34,16 @@
+ [required]="true"> {{(direction === entitySearchDirection.FROM ? 'relation.to-entity' : 'relation.from-entity') | translate}}
-
+
{{(direction == directions.FROM ? 'relation.from-relations' : 'relation.to-relations') | translate}} - + relation.direction diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.html index d4213b95e5..a4a57c52e7 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.html @@ -17,7 +17,7 @@ -->
{{ title }}
- + rule-node-config.save-time-series.strategy @for (strategy of processingStrategies; track strategy) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts index 22cdec0c82..c569d3270f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting-row.component.ts @@ -32,6 +32,7 @@ import { } from '@home/components/rule-node/action/timeseries-config.models'; import { isDefinedAndNotNull } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-advanced-processing-setting-row', @@ -52,6 +53,9 @@ export class AdvancedProcessingSettingRowComponent implements ControlValueAccess @Input() title: string; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + processingSettingRowForm = this.fb.group({ type: [defaultAdvancedProcessingConfig.type], deduplicationIntervalSecs: [{value: 60, disabled: true}] diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html index 5b403ff611..0136d08250 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html @@ -24,30 +24,35 @@ @if (timeseries) { } @if (attributes) { } @if (latest) { } @if (webSockets) { } @if (calculatedFields) { } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html index 2ac22c6284..64a23c30e6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/assign-customer-config.component.html @@ -17,7 +17,7 @@ -->
- + rule-node-config.customer-name-pattern @if (assignCustomerConfigForm.get('customerNamePattern').hasError('required') || diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html index 50444e3a87..51ac7c34b6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html @@ -28,7 +28,7 @@
@if(!attributesConfigForm.get('processingSettings.isAdvanced').value) { - + rule-node-config.save-attribute.strategy @for (strategy of processingStrategies; track strategy) { @@ -39,6 +39,7 @@ @if(attributesConfigForm.get('processingSettings.type').value === ProcessingType.DEDUPLICATE) {
- + {{ 'rule-node-config.attributes-scope' | translate }} @@ -74,7 +75,7 @@ } - + {{ 'rule-node-config.attributes-scope-value' | translate }}
- + rule-node-config.alarm-type @if (clearAlarmConfigForm.get('alarmType').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html index eba427c385..eea361f6c8 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-alarm-config.component.html @@ -77,7 +77,7 @@ } @if (createAlarmConfigForm.get('useMessageAlarmData').value === false) {
- + rule-node-config.alarm-type @if (createAlarmConfigForm.get('alarmType').hasError('required')) { @@ -87,11 +87,11 @@ } rule-node-config.general-pattern-hint - + {{ 'rule-node-config.use-alarm-severity-pattern' | translate }} @if (!createAlarmConfigForm.get('dynamicSeverity').value) { - + rule-node-config.alarm-severity @for (severity of alarmSeverities; track severity) { @@ -108,7 +108,7 @@ } @if (createAlarmConfigForm.get('dynamicSeverity').value) { - + rule-node-config.alarm-severity-pattern @if (createAlarmConfigForm.get('severity').hasError('required')) { @@ -124,7 +124,7 @@ @if (createAlarmConfigForm.get('propagate').value === true) {
- + rule-node-config.relation-types-list @for (key of createAlarmConfigForm.get('relationTypes').value; track key) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html index 817ff84b53..a50b62b26d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/create-relation-config.component.html @@ -19,7 +19,7 @@
rule-node-config.relation-parameters
- + relation.direction @for (type of directionTypes; track type) { @@ -30,6 +30,7 @@ @@ -40,6 +41,7 @@
rule-node-config.target-entity
{{ entityTypeNamePatternTranslation.get(createRelationConfigForm.get('entityType').value) | translate }} @@ -59,6 +62,7 @@ @if (createRelationConfigForm.get('entityType').value === entityType.DEVICE || createRelationConfigForm.get('entityType').value === entityType.ASSET) { rule-node-config.profile-name diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html index cb0b89d1ed..a260f16b96 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-attributes-config.component.html @@ -20,7 +20,7 @@
- + {{ 'rule-node-config.attributes-scope' | translate }} @@ -31,7 +31,7 @@ } - + {{ 'rule-node-config.attributes-scope-value' | translate }}
- + {{ 'rule-node-config.attributes-keys' | translate }} @for (key of deleteAttributesConfigForm.get('keys').value; track key) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html index 7899e4773d..5c1517804d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/delete-relation-config.component.html @@ -19,7 +19,7 @@
rule-node-config.relation-parameters
- + relation.direction @for (type of directionTypes; track type) { @@ -30,6 +30,7 @@ @@ -49,11 +50,13 @@ class="flex-1" showLabel required + appearance="outline" [allowedEntityTypes]="allowedEntityTypes" formControlName="entityType"> @if (deleteRelationConfigForm.get('entityType').value && deleteRelationConfigForm.get('entityType').value !== entityType.TENANT) { {{ entityTypeNamePatternTranslation.get(deleteRelationConfigForm.get('entityType').value) | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html index cb3d6d2b7e..bdb412f385 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/device-state-config.component.html @@ -16,7 +16,7 @@ -->
- + {{ 'rule-node-config.select-device-connectivity-event' | translate }} @for (eventOption of eventOptions; track eventOption) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html index cea850d0be..d0d91e0bb1 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.html @@ -19,7 +19,7 @@
rule-node-config.generation-parameters
- + rule-node-config.message-count @if (generatorConfigForm.get('msgCount').hasError('required')) { @@ -33,7 +33,7 @@ } - + rule-node-config.generation-frequency-seconds @if (generatorConfigForm.get('periodInSeconds').hasError('required')) { @@ -53,6 +53,7 @@
rule-node-config.originator
rule-node-config.coordinate-field-names
- + {{ 'rule-node-config.latitude-field-name' | translate }} @if (geoActionConfigForm.get('latitudeKeyName').hasError('required')) { @@ -29,7 +29,7 @@ } - + {{ 'rule-node-config.longitude-field-name' | translate }} @if (geoActionConfigForm.get('longitudeKeyName').hasError('required')) { @@ -45,7 +45,7 @@
rule-node-config.geofence-configuration
- + {{ 'rule-node-config.perimeter-type' | translate }} @for (type of perimeterTypes; track type) { @@ -70,7 +70,7 @@
@if (geoActionConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { - + {{ 'rule-node-config.perimeter-key-name' | translate }} @if (geoActionConfigForm.get('perimeterKeyName').hasError('required')) { @@ -83,8 +83,7 @@ } @if (geoActionConfigForm.get('perimeterType').value === perimeterType.CIRCLE && !geoActionConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { -
+
{{ 'rule-node-config.circle-center-latitude' | translate }} @@ -95,7 +94,7 @@ } - + {{ 'rule-node-config.circle-center-longitude' | translate }} @if (geoActionConfigForm.get('centerLongitude').hasError('required')) { @@ -106,7 +105,7 @@
- + {{ 'rule-node-config.range' | translate }} @if (geoActionConfigForm.get('range').hasError('required')) { @@ -115,7 +114,7 @@ } - + {{ 'rule-node-config.range-units' | translate }} @for (type of rangeUnits; track type) { @@ -136,7 +135,7 @@ @if (geoActionConfigForm.get('perimeterType').value === perimeterType.POLYGON && !geoActionConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) {
- + rule-node-config.polygon-definition
- + rule-node-config.min-inside-duration @if (geoActionConfigForm.get('minInsideDuration').hasError('required')) { @@ -193,7 +192,7 @@ } - + rule-node-config.min-inside-duration-time-unit @for (timeUnit of timeUnits; track timeUnit) { @@ -205,7 +204,7 @@
- + rule-node-config.min-outside-duration @if (geoActionConfigForm.get('minOutsideDuration').hasError('required')) { @@ -224,7 +223,7 @@ } - + rule-node-config.min-outside-duration-time-unit @for (timeUnit of timeUnits; track timeUnit) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html index bd5fad7679..302b3828ba 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html @@ -19,6 +19,7 @@
@@ -31,7 +32,7 @@
{{'rule-node-config.custom-expression-field-input' | translate }} * - +
rule-node-config.result-title
- + rule-node-config.type-field-input @@ -75,7 +76,7 @@
@if (mathFunctionConfigForm.get('result').get('type').value === ArgumentTypeResult.ATTRIBUTE) { - + rule-node-config.attribute-scope-field-input @for (scope of attributeScopeResult; track scope) { @@ -86,7 +87,7 @@ } - + rule-node-config.key-field-input
- + rule-node-config.number-floating-point-field-input
@if ([ArgumentTypeResult.ATTRIBUTE, ArgumentTypeResult.TIME_SERIES].includes(mathFunctionConfigForm.get('result').get('type').value)) { -
+
{{'rule-node-config.add-to-message-field-input' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.html index 31010602c3..305ce7615b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-count-config.component.html @@ -16,7 +16,7 @@ -->
- + rule-node-config.interval-seconds @if (msgCountConfigForm.get('interval').hasError('required')) { @@ -30,7 +30,7 @@ } - + rule-node-config.output-timeseries-key-prefix @if (msgCountConfigForm.get('telemetryPrefix').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.html index a7dc7b7b1b..1b23da7dea 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/msg-delay-config.component.html @@ -22,6 +22,7 @@
rule-node-config.use-metadata-period-in-seconds-patterns-hint
@if (msgDelayConfigForm.get('useMetadataPeriodInSecondsPatterns').value !== true) { rule-node-config.period-seconds @@ -37,7 +38,7 @@ } } @else { - + rule-node-config.period-in-seconds-pattern @if (msgDelayConfigForm.get('periodInSecondsPattern').hasError('required')) { @@ -48,7 +49,7 @@ rule-node-config.general-pattern-hint } - + rule-node-config.max-pending-messages @if (msgDelayConfigForm.get('maxPendingMsgs').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.html index 5a39dae562..f4e312fe00 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/push-to-cloud-config.component.html @@ -20,7 +20,7 @@
- + {{ 'rule-node-config.attributes-scope' | translate }} @@ -31,7 +31,7 @@ } - + {{ 'rule-node-config.attributes-scope-value' | translate }}
@if (unassignCustomerConfigForm.get('unassignFromCustomer').value) { - + rule-node-config.customer-name-pattern @if (unassignCustomerConfigForm.get('customerNamePattern').hasError('required') || diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html index dd5579b230..664bc64aad 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html @@ -24,7 +24,7 @@ - + rule-node-config.credentials-type @for (credentialsType of allCredentialsTypes; track credentialsType) { @@ -44,7 +44,7 @@
@switch (credentialsConfigFormGroup.get('type').value) { @case("basic") { - + rule-node-config.username @if (credentialsConfigFormGroup.get('username').hasError('required')) { @@ -53,7 +53,7 @@ } - + rule-node-config.password diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html index 380668f9fd..6a25b5e6cb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html @@ -17,7 +17,7 @@ -->
- + relation.direction @for (type of directionTypes; track type) { @@ -27,7 +27,7 @@ } - + rule-node-config.max-relation-level } - + rule-node-config.functions-field-input - + @if (label) { {{ label }} } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html index 138d45700b..035a739918 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.html @@ -16,7 +16,7 @@ -->
- + {{'rule-node-config.output-message-type' | translate}} @for (msgType of messageTypes; track msgType) { @@ -26,7 +26,7 @@ } - + {{'rule-node-config.message-type-value' | translate}} @if (messageTypeFormGroup.get('messageType').value) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts index 5cb9922fcb..2ded7151c8 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/output-message-type-autocomplete.component.ts @@ -24,7 +24,7 @@ import { Validator, Validators } from '@angular/forms'; -import { SubscriptSizing } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { coerceBoolean } from '@shared/public-api'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -61,6 +61,9 @@ export class OutputMessageTypeAutocompleteComponent implements ControlValueAcces @coerceBoolean() disabled: boolean; + @Input() + appearance: MatFormFieldAppearance = 'fill' + @Input() @coerceBoolean() set required(value) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html index 5f8754c723..01ec5c5e9a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/relations-query-config.component.html @@ -19,7 +19,7 @@
rule-node-config.relations-query
- + relation.direction @for (type of directionTypes; track type) { @@ -29,7 +29,7 @@ } - + rule-node-config.max-relation-level diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html index dcc5fee728..6011ea79c5 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/calculate-delta-config.component.html @@ -17,7 +17,7 @@ -->
- + {{ 'rule-node-config.input-value-key' | translate }} @if (calculateDeltaConfigForm.get('inputValueKey').hasError('required') || @@ -27,7 +27,7 @@ } - + {{ 'rule-node-config.output-value-key' | translate }} @if (calculateDeltaConfigForm.get('outputValueKey').hasError('required') || @@ -38,7 +38,7 @@ }
- + {{ 'rule-node-config.number-of-digits-after-floating-point' | translate }} @if (calculateDeltaConfigForm.get('round').hasError('min')) { @@ -80,7 +80,7 @@
@if (calculateDeltaConfigForm.get('addPeriodBetweenMsgs').value) { - + {{ 'rule-node-config.period-value-key' | translate }} @if (calculateDeltaConfigForm.get('periodValueKey').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html index ff7939cab4..50af390ff8 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html @@ -18,6 +18,7 @@
@@ -40,10 +41,9 @@
@if (getTelemetryFromDatabaseConfigForm.get('useMetadataIntervalPatterns').value === false) { -
+
- + {{ 'rule-node-config.interval-start' | translate }} @if (getTelemetryFromDatabaseConfigForm.get('interval.startInterval').hasError('required')) { @@ -62,7 +62,7 @@ } - + {{ 'rule-node-config.time-unit' | translate }} @for (timeUnit of timeUnits; track timeUnit) { @@ -74,7 +74,7 @@
- + {{ 'rule-node-config.interval-end' | translate }} @if (getTelemetryFromDatabaseConfigForm.get('interval.endInterval').hasError('required')) { @@ -93,7 +93,7 @@ } - + {{ 'rule-node-config.time-unit' | translate }} @for (timeUnit of timeUnits; track timeUnit) { @@ -123,7 +123,7 @@
} @else {
- + {{ 'rule-node-config.start-interval' | translate }} @if (getTelemetryFromDatabaseConfigForm.get('startIntervalPattern').hasError('required') || @@ -133,7 +133,7 @@ } - + {{ 'rule-node-config.end-interval' | translate }} @if (getTelemetryFromDatabaseConfigForm.get('endIntervalPattern').hasError('required') || @@ -167,7 +167,7 @@
@if (getTelemetryFromDatabaseConfigForm.get('fetchMode').value === fetchMode.ALL) {
- {{ 'aggregation.function' | translate }} @@ -180,7 +180,7 @@ @if (getTelemetryFromDatabaseConfigForm.get('aggregation').value === aggregationTypes.NONE) {
- + {{ "rule-node-config.order-by-timestamp" | translate }} @for (order of samplingOrders; track order) { @@ -190,7 +190,7 @@ } - + {{ "rule-node-config.limit" | translate }} {{ "rule-node-config.limit-hint" | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index 7167b68132..9b0a51d7ec 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -82,6 +82,7 @@
- + rule-node-config.topic @if (azureIotHubConfigForm.get('topicPattern').hasError('required')) { @@ -26,7 +26,7 @@ } rule-node-config.general-pattern-hint - + rule-node-config.hostname @if (azureIotHubConfigForm.get('host').hasError('required')) { @@ -35,7 +35,7 @@ } - + rule-node-config.device-id @if (azureIotHubConfigForm.get('clientId').hasError('required')) { @@ -44,7 +44,7 @@ } - @@ -56,7 +56,7 @@
- + rule-node-config.credentials-type @for (credentialsType of allAzureIotHubCredentialsTypes; track credentialsType) { @@ -75,7 +75,7 @@ @switch (azureIotHubConfigForm.get('credentials.type').value) { @case ('anonymous') {} @case ('sas') { - + rule-node-config.sas-key @@ -124,7 +124,7 @@ noFileText="rule-node-config.no-file" dropLabel="{{'rule-node-config.drop-file' | translate}}"> - + rule-node-config.private-key-password diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html index ffbc99fe57..29e143a2f0 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html @@ -16,7 +16,7 @@ -->
- + rule-node-config.topic-pattern @if (kafkaConfigForm.get('topicPattern').hasError('required')) { @@ -26,13 +26,13 @@ } rule-node-config.general-pattern-hint - + rule-node-config.key-pattern rule-node-config.general-pattern-hint
rule-node-config.key-pattern-hint
- + rule-node-config.bootstrap-servers @if (kafkaConfigForm.get('bootstrapServers').hasError('required')) { @@ -41,7 +41,7 @@ } - + rule-node-config.retries @if (kafkaConfigForm.get('retries').hasError('min')) { @@ -50,7 +50,7 @@ } - + rule-node-config.batch-size-bytes @if (kafkaConfigForm.get('batchSize').hasError('min')) { @@ -59,7 +59,7 @@ } - + rule-node-config.linger-ms @if (kafkaConfigForm.get('linger').hasError('min')) { @@ -68,7 +68,7 @@ } - + rule-node-config.buffer-memory-bytes @if (kafkaConfigForm.get('bufferMemory').hasError('min')) { @@ -77,7 +77,7 @@ } - + rule-node-config.acks @for (ackValue of ackValues; track ackValue) { @@ -101,7 +101,7 @@
rule-node-config.add-metadata-key-values-as-kafka-headers-hint
@if (kafkaConfigForm.get('addMetadataKeyValuesAsKafkaHeaders').value) { - + rule-node-config.charset-encoding @for (charset of ToByteStandartCharsetTypesValues; track charset) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html index fba6b87d72..4e80aad77b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html @@ -24,7 +24,7 @@ popupHelpLink="rulenode/node-templatization-doc">
- + {{'rule-node-config.function-name' | translate}} @if (lambdaConfigForm.get('functionName').hasError('required')) { @@ -33,7 +33,7 @@ } - + {{'rule-node-config.qualifier' | translate}} rule-node-config.qualifier-hint @@ -47,7 +47,7 @@ rule-node-config.aws-credentials
- + rule-node-config.aws-access-key-id @if (lambdaConfigForm.get('accessKey').hasError('required')) { @@ -56,7 +56,7 @@ } - + rule-node-config.aws-secret-access-key @if (lambdaConfigForm.get('secretKey').hasError('required')) { @@ -65,7 +65,7 @@ } - + rule-node-config.aws-region @if (lambdaConfigForm.get('region').hasError('required')) { @@ -84,7 +84,7 @@
- + rule-node-config.connection-timeout @if (lambdaConfigForm.get('connectionTimeout').hasError('required')) { @@ -101,7 +101,7 @@ color="primary" matTooltip="{{ 'rule-node-config.connection-timeout-hint' | translate }}">help - + rule-node-config.request-timeout @if (lambdaConfigForm.get('requestTimeout').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/mqtt-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/mqtt-config.component.html index e756cdb961..127e67db0d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/mqtt-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/mqtt-config.component.html @@ -16,7 +16,7 @@ -->
- + rule-node-config.topic-pattern @if (mqttConfigForm.get('topicPattern').hasError('required')) { @@ -27,7 +27,7 @@ rule-node-config.general-pattern-hint
- + rule-node-config.host @if (mqttConfigForm.get('host').hasError('required')) { @@ -36,7 +36,7 @@ } - + rule-node-config.port @if (mqttConfigForm.get('port').hasError('required')) { @@ -55,7 +55,7 @@ } - + rule-node-config.connect-timeout @if (mqttConfigForm.get('connectTimeoutSec').hasError('required')) { @@ -75,7 +75,7 @@ }
- + rule-node-config.client-id {{'rule-node-config.client-id-hint' | translate}} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.html index ce937fe7d7..8101d2b2a6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/notification-config.component.html @@ -19,12 +19,14 @@
- + rule-node-config.gcp-project-id @if (pubSubConfigForm.get('projectId').hasError('required')) { @@ -25,7 +25,7 @@ } - + rule-node-config.pubsub-topic-name @if (pubSubConfigForm.get('topicName').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html index 19f06ef77d..eb73a662a3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html @@ -16,15 +16,15 @@ -->
- + rule-node-config.exchange-name-pattern - + rule-node-config.routing-key-pattern - + rule-node-config.message-properties @for (property of messageProperties; track property) { @@ -35,7 +35,7 @@
- + rule-node-config.host @if (rabbitMqConfigForm.get('host').hasError('required')) { @@ -44,7 +44,7 @@ } - + rule-node-config.port @if (rabbitMqConfigForm.get('port').hasError('required')) { @@ -64,23 +64,23 @@ }
- + rule-node-config.virtual-host - + rule-node-config.username - + rule-node-config.password - + {{ 'rule-node-config.automatic-recovery' | translate }} - + rule-node-config.connection-timeout-ms @if (rabbitMqConfigForm.get('connectionTimeout').hasError('min')) { @@ -89,7 +89,7 @@ } - + rule-node-config.handshake-timeout-ms @if (rabbitMqConfigForm.get('handshakeTimeout').hasError('min')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html index 3b2a6a1cb6..7fc23ce3e2 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rest-api-call-config.component.html @@ -18,7 +18,7 @@
rule-node-config.request-url
- + rule-node-config.url @if (restApiCallConfigForm.get('restEndpointUrlPattern').hasError('required')) { @@ -45,7 +45,7 @@
- + rule-node-config.request-method @for (requestType of httpRequestTypes; track requestType) { @@ -92,7 +92,7 @@ @if (!restApiCallConfigForm.get('useSystemProxyProperties').value) {
- + rule-node-config.proxy-host @if (restApiCallConfigForm.get('proxyHost').hasError('required')) { @@ -101,7 +101,7 @@ } - + rule-node-config.proxy-port @if (restApiCallConfigForm.get('proxyPort').hasError('required')) { @@ -116,11 +116,11 @@ }
- + rule-node-config.proxy-user - + rule-node-config.proxy-password @@ -142,7 +142,7 @@ {{ 'rule-node-config.ignore-request-body' | translate }}
- + rule-node-config.read-timeout rule-node-config.read-timeout-hint @@ -152,7 +152,7 @@ } - + rule-node-config.max-parallel-requests-count rule-node-config.max-parallel-requests-count-hint @@ -162,7 +162,7 @@ } - + rule-node-config.max-response-size rule-node-config.max-response-size-hint diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html index 295de6912a..9f5f9a590f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html @@ -21,7 +21,7 @@ @if (sendEmailConfigForm.get('useSystemSmtpSettings').value === false) {
- + rule-node-config.smtp-protocol @for (smtpProtocol of smtpProtocols; track smtpProtocol) { @@ -32,7 +32,7 @@
- + rule-node-config.smtp-host @if (sendEmailConfigForm.get('smtpHost').hasError('required')) { @@ -41,7 +41,7 @@ } - + rule-node-config.smtp-port @if (sendEmailConfigForm.get('smtpPort').hasError('required')) { @@ -61,7 +61,7 @@ }
- + rule-node-config.timeout-msec @if (sendEmailConfigForm.get('timeout').hasError('required')) { @@ -79,7 +79,7 @@ {{ 'rule-node-config.enable-tls' | translate }} @if (sendEmailConfigForm.get('enableTls').value === true) { - + rule-node-config.tls-version @for (tlsVersion of tlsVersions; track tlsVersion) { @@ -96,7 +96,7 @@ @if (sendEmailConfigForm.get('enableProxy').value) {
- + rule-node-config.proxy-host @if (sendEmailConfigForm.get('proxyHost').hasError('required')) { @@ -105,7 +105,7 @@ } - + rule-node-config.proxy-port @if (sendEmailConfigForm.get('proxyPort').hasError('required')) { @@ -120,21 +120,21 @@ }
- + rule-node-config.proxy-user - + rule-node-config.proxy-password
} - + rule-node-config.username - + rule-node-config.password diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html index ff9a77a3f5..d9136b3cb6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-sms-config.component.html @@ -16,7 +16,7 @@ -->
- + rule-node-config.numbers-to-template @if (sendSmsConfigForm.get('numbersToTemplate').hasError('required')) { @@ -26,9 +26,9 @@ } - + rule-node-config.sms-message-template - + @if (sendSmsConfigForm.get('smsMessageTemplate').hasError('required')) { {{ 'rule-node-config.sms-message-template-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html index 9ef122ad9e..31f91ba86a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/slack-config.component.html @@ -16,7 +16,7 @@ -->
- + rule-node-config.message-template @if (slackConfigForm.get('messageTemplate').hasError('required')) { @@ -30,7 +30,7 @@ {{ 'rule-node-config.use-system-slack-settings' | translate }} @if (!slackConfigForm.get('useSystemSettings').value) { - + rule-node-config.slack-api-token @if (slackConfigForm.get('botToken').hasError('required')) { @@ -49,6 +49,7 @@ }
- + rule-node-config.topic-arn-pattern @if (snsConfigForm.get('topicArnPattern').hasError('required')) { @@ -26,7 +26,7 @@ } rule-node-config.general-pattern-hint - + rule-node-config.aws-access-key-id @if (snsConfigForm.get('accessKeyId').hasError('required')) { @@ -35,7 +35,7 @@ } - + rule-node-config.aws-secret-access-key @if (snsConfigForm.get('secretAccessKey').hasError('required')) { @@ -44,7 +44,7 @@ } - + rule-node-config.aws-region @if (snsConfigForm.get('region').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html index 0e07578f19..98ca8b93d8 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/sqs-config.component.html @@ -16,7 +16,7 @@ -->
- + rule-node-config.queue-type @for (type of sqsQueueTypes; track type) { @@ -26,7 +26,7 @@ } - + rule-node-config.queue-url-pattern @if (sqsConfigForm.get('queueUrlPattern').hasError('required')) { @@ -37,7 +37,7 @@ rule-node-config.general-pattern-hint @if (sqsConfigForm.get('queueType').value === sqsQueueType.STANDARD) { - + rule-node-config.delay-seconds @if (sqsConfigForm.get('delaySeconds').hasError('min')) { @@ -62,7 +62,7 @@ valText="rule-node-config.value" valRequiredText="rule-node-config.value-required"> - + rule-node-config.aws-access-key-id @if (sqsConfigForm.get('accessKeyId').hasError('required')) { @@ -71,7 +71,7 @@ } - + rule-node-config.aws-secret-access-key @if (sqsConfigForm.get('secretAccessKey').hasError('required')) { @@ -80,7 +80,7 @@ } - + rule-node-config.aws-region @if (sqsConfigForm.get('region').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html index c4cd8f18ed..ae5a957bf6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html @@ -23,6 +23,7 @@
@@ -30,6 +31,7 @@ matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.field-name' | translate } }}">help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html index 3e2773037d..6aee9bf101 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.html @@ -18,7 +18,7 @@
rule-node-config.relation-search-parameters
- + {{ 'relation.direction' | translate }} @for (direction of entitySearchDirection; track direction) { @@ -29,6 +29,7 @@ @@ -41,6 +42,7 @@ @if (checkRelationConfigForm.get('checkForSingleEntity').value) {
@if (checkRelationConfigForm.get('entityType').value) { rule-node-config.coordinate-field-names
- + {{ 'rule-node-config.latitude-field-name' | translate }} @if (geoFilterConfigForm.get('latitudeKeyName').hasError('required')) { @@ -29,7 +29,7 @@ } - + {{ 'rule-node-config.longitude-field-name' | translate }} @if (geoFilterConfigForm.get('longitudeKeyName').hasError('required')) { @@ -45,7 +45,7 @@
rule-node-config.geofence-configuration
- + {{ 'rule-node-config.perimeter-type' | translate }} @for (type of perimeterTypes; track type) { @@ -70,7 +70,7 @@
@if (geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { - + {{ 'rule-node-config.perimeter-key-name' | translate }} @if (geoFilterConfigForm.get('perimeterKeyName').hasError('required')) { @@ -83,10 +83,9 @@ } @if (geoFilterConfigForm.get('perimeterType').value === perimeterType.CIRCLE && !geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { -
+
- + {{ 'rule-node-config.circle-center-latitude' | translate }} @if (geoFilterConfigForm.get('centerLatitude').hasError('required')) { @@ -95,7 +94,7 @@ } - + {{ 'rule-node-config.circle-center-longitude' | translate }} @if (geoFilterConfigForm.get('centerLongitude').hasError('required')) { @@ -106,7 +105,7 @@
- + {{ 'rule-node-config.range' | translate }} @if (geoFilterConfigForm.get('range').hasError('required')) { @@ -115,7 +114,7 @@ } - + {{ 'rule-node-config.range-units' | translate }} @for (type of rangeUnits; track type) { @@ -135,8 +134,7 @@ } @if (geoFilterConfigForm.get('perimeterType').value === perimeterType.POLYGON && !geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value) { - + {{ 'rule-node-config.polygon-definition' | translate }} {{ 'rule-node-config.polygon-definition-hint' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html index a0d90bab4a..b94519d914 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html @@ -17,6 +17,7 @@ -->
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html index 22d58fb9fc..efcea2eb7b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/change-originator-config.component.html @@ -16,7 +16,7 @@ -->
- + rule-node-config.new-originator @@ -40,8 +40,7 @@ @if (changeOriginatorConfigForm.get('originatorSource').value === originatorSource.ENTITY) { -
+
@@ -49,11 +48,12 @@ - + rule-node-config.entity-name-pattern @if (changeOriginatorConfigForm.get('entityNamePattern').hasError('required') || diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html index 0fc80e6197..a899a344b3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html @@ -23,6 +23,7 @@
- + {{'rule-node-config.interval' | translate}} @if (deduplicationConfigForm.get('interval').hasError('required')) { @@ -44,26 +44,24 @@ } @if (deduplicationConfigForm.get('strategy').value === 'ALL') { - + } @if (deduplicationConfigForm.get('strategy').value === 'FIRST') { + textAlign="'center'"> } @if (deduplicationConfigForm.get('strategy').value === 'LAST') { + textAlign="'center'"> } @if (deduplicationConfigForm.get('strategy').value === deduplicationStrategie.ALL) {
@@ -75,7 +73,7 @@ rule-node-config.advanced-settings
- + {{'rule-node-config.max-pending-msgs' | translate}} @if (deduplicationConfigForm.get('maxPendingMsgs').hasError('required')) { @@ -97,7 +95,7 @@ color="primary" matTooltip="{{ 'rule-node-config.max-pending-msgs-hint' | translate }}">help - + {{'rule-node-config.max-retries' | translate}} @if (deduplicationConfigForm.get('maxRetries').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html index c5d94739d6..4da3fdf45c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html @@ -22,6 +22,7 @@
- + {{ 'rule-node-config.json-path-expression' | translate }} {{ 'rule-node-config.json-path-expression-hint' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/to-email-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/to-email-config.component.html index f2d8e253ec..118faf6657 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/to-email-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/to-email-config.component.html @@ -19,7 +19,7 @@
rule-node-config.email-sender
- + rule-node-config.from-template @@ -52,7 +52,7 @@
- + rule-node-config.to-template - + rule-node-config.bcc-template - @if (entity?.id) {
} + + + tenant.description + +
-
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index 84744f6217..685f0f3336 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -236,16 +236,9 @@ [(ngModel)]="widget.image" (ngModelChange)="isDirty = true" > - - widget.description - - {{descriptionInput.value?.length || 0}}/1024 - @@ -261,20 +254,20 @@ {{ 'widget.deprecated' | translate }}
- + widget.settings-form-selector - + widget.data-key-settings-form-selector @if (widget.type === widgetTypes.timeseries) { - + widget.latest-data-key-settings-form-selector @if (widget.hasBasicMode) { - + widget.basic-mode-form-selector } + + widget.description + + {{descriptionInput.value?.length || 0}}/1024 +
diff --git a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html index 16ef833ec3..98faaac2f2 100644 --- a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html @@ -34,9 +34,10 @@
-
+
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html index 6a94f1fc04..fb7eac1806 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html @@ -27,6 +27,7 @@ [allowedEntityTypes]="allowedEntityTypes" [filterAllowedEntityTypes]="filterAllowedEntityTypes" [additionEntityTypes]="additionEntityTypes" + [appearance]="appearance" formControlName="entityType"> } @@ -38,6 +39,7 @@ [required]="required" [entityType]="modelValue.entityType" [useEntityDisplayName]="useEntityDisplayName" + [appearance]="appearance" formControlName="entityIds"> } diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts index 7ceaee14d7..1bf532af2e 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts @@ -21,6 +21,7 @@ import { EntityService } from '@core/http/entity.service'; import { EntityId } from '@shared/models/id/entity-id'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { isDefinedAndNotNull } from '@core/utils'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; interface EntityListSelectModel { entityType: EntityType | AliasEntityType; @@ -72,6 +73,9 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit { @Input({transform: booleanAttribute}) useEntityDisplayName = false; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + displayEntityTypeSelect: boolean; private defaultEntityType: EntityType | AliasEntityType = null; diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.html b/ui-ngx/src/app/shared/components/image/image-dialog.component.html index ee830aa102..0fe1a4be59 100644 --- a/ui-ngx/src/app/shared/components/image/image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.html @@ -33,7 +33,7 @@ }
- + image.name @if (imageFormGroup.get('title').hasError('required')) { diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.html b/ui-ngx/src/app/shared/components/material-icon-select.component.html index dc22921c7e..55e76946d6 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.html +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -18,7 +18,7 @@ @if (!asBoxInput) {
{{materialIconFormGroup.get('icon').value}} - + {{ label }} @if (iconClearButton) { diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index a0f5387a24..ec040bd488 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -36,6 +36,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { MaterialIconsComponent } from '@shared/components/material-icons.component'; import { MatButton } from '@angular/material/button'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-material-icon-select', @@ -76,6 +77,9 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @coerceBoolean() allowedCustomIcon = false; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; diff --git a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html index fb82ed8032..c63cbf82fb 100644 --- a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{ 'rulenode.message-type' | translate }} - settings_ethernet From 1cedb66138e344c2d39aad16cd20803809f50016 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 2 Apr 2026 13:17:05 +0200 Subject: [PATCH 222/313] Additional inputs reworks --- .../dashboard-state-dialog.component.html | 4 +- ...snmp-device-profile-mapping.component.html | 9 ++-- .../common/kv-map-config-old.component.html | 4 +- ...command-button-basic-config.component.html | 2 +- .../power-button-basic-config.component.html | 2 +- .../toggle-button-basic-config.component.html | 2 +- .../status-widget-basic-config.component.html | 2 +- .../single-switch-basic-config.component.html | 2 +- .../rpc/slider-basic-config.component.html | 2 +- .../value-stepper-basic-config.component.html | 2 +- .../scada-symbol-basic-config.component.html | 2 +- .../config/target-device.component.html | 2 + .../widget/config/target-device.component.ts | 4 ++ .../date-range-navigator-panel.component.html | 2 +- .../date-range-navigator.component.html | 6 +-- .../lib/home-page/doc-link.component.html | 6 +-- .../lib/home-page/quick-link.component.html | 2 +- .../rpc/persistent-add-dialog.component.html | 6 +-- .../persistent-details-dialog.component.html | 13 +++--- .../persistent-filter-panel.component.html | 2 +- ...board-state-widget-settings.component.html | 5 ++- .../cards/label-widget-label.component.html | 9 ++-- .../qrcode-widget-settings.component.html | 2 +- .../common/widget-font.component.html | 12 +++--- .../device-key-autocomplete.component.html | 2 +- .../device-key-autocomplete.component.ts | 4 ++ ...d-indicator-widget-settings.component.html | 12 +++--- ...stent-table-widget-settings.component.html | 4 +- ...ound-switch-widget-settings.component.html | 2 +- .../control/rpc-button-style.component.html | 2 + .../rpc-shell-widget-settings.component.html | 2 +- ...pc-terminal-widget-settings.component.html | 4 +- .../send-rpc-widget-settings.component.html | 10 ++--- ...lide-toggle-widget-settings.component.html | 4 +- ...tch-control-widget-settings.component.html | 2 +- .../switch-rpc-settings.component.html | 11 ++--- ...e-attribute-widget-settings.component.html | 9 ++-- ...e-navigator-widget-settings.component.html | 6 +-- ...eway-config-widget-settings.component.html | 13 +++--- ...eway-events-widget-settings.component.html | 3 +- ...pio-control-widget-settings.component.html | 7 ++-- .../settings/gpio/gpio-item.component.html | 9 ++-- ...quick-links-widget-settings.component.html | 2 +- .../datakey-select-option.component.html | 4 +- ...ce-claiming-widget-settings.component.html | 18 ++++---- ...e-attribute-widget-settings.component.html | 5 ++- ...n-attribute-widget-settings.component.html | 17 ++++---- ...ple-attributes-key-settings.component.html | 41 ++++++++++--------- ...-attributes-widget-settings.component.html | 19 +++++---- ...gation-card-widget-settings.component.html | 5 ++- ...ation-cards-widget-settings.component.html | 2 +- .../widget/widget-config.component.html | 2 +- .../admin/send-test-sms-dialog.component.html | 3 +- .../device-profile-tabs.component.html | 2 +- ...ice-transport-configuration.component.html | 24 +++++------ ...ate-nested-rulechain-dialog.component.html | 4 +- .../rulechain/link-labels.component.html | 2 +- .../email-auth-dialog.component.html | 4 +- .../sms-auth-dialog.component.html | 3 +- .../totp-auth-dialog.component.html | 2 +- .../save-widget-type-as-dialog.component.html | 3 +- .../widget/widgets-bundle.component.html | 14 +++---- ...force-two-factor-auth-login.component.html | 1 + .../components/color-input.component.html | 2 +- .../components/color-input.component.ts | 6 ++- .../image/upload-image-dialog.component.html | 2 +- .../widgets-bundle-select.component.html | 2 +- .../widgets-bundle-select.component.ts | 4 ++ 68 files changed, 222 insertions(+), 181 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html index ad0aeb097c..58cb21253d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html @@ -31,7 +31,7 @@ }
- + dashboard.state-name @if (stateFormGroup.get('name').hasError('required')) { @@ -40,7 +40,7 @@ } - + dashboard.state-id @if (stateFormGroup.get('id').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html index acd00e3707..2f0ef4d0af 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html @@ -26,10 +26,9 @@
@for (mappingConfig of mappingsConfigFormArray.controls; track mappingConfig; let isLast = $last) { -
+
- + @for (dataType of dataTypes; track dataType) { @@ -43,7 +42,7 @@ } - + @if (mappingConfig.get('key').hasError('required')) { @@ -51,7 +50,7 @@ } - + @if (mappingConfig.get('oid').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html index 3e75dc69ec..831630074a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/kv-map-config-old.component.html @@ -31,7 +31,7 @@
- + @if (keyValControl.get('key').hasError('required')) { @@ -40,7 +40,7 @@ } - + @if (keyValControl.get('value').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.html index 34ce199e65..e39d57b64e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/command-button-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.command-button.behavior
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html index 71edff2c83..8d05bad66d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.power-button.behavior
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.html index d506fe4e8b..905e9e3bac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/toggle-button-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.toggle-button.behavior
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html index 450f8227fa..c64a4e23d9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.status-widget.behavior
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html index 8b34ffa0c8..1ed4ec86a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.single-switch.behavior
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html index d9bd4b82f2..ff5397ea16 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.slider.behavior
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html index d2d1e65491..986af5d80e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
widgets.value-stepper.behavior
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.html index 03cdee6b95..fa9780cd06 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.html @@ -16,7 +16,7 @@ --> - +
scada.symbol.symbol
@if (targetDeviceFormGroup.get('type').value === targetDeviceType.device) { @@ -32,6 +33,7 @@ } @if (targetDeviceFormGroup.get('type').value === targetDeviceType.entity) {
@if (settings.showTemplate) { - + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html index 3b5990a268..5cdc878b57 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.html @@ -19,7 +19,7 @@ [class.short-mode]="ctx.width < 400" [class.labels-hidden]="settings.hideLabels" [class.long-mode]="ctx.width >= 400"> - + @if (!settings.hideLabels) { widgets.date-range-navigator.localizationMap.Date picker } @@ -29,7 +29,7 @@ - + @if (!settings.hideLabels) { widgets.date-range-navigator.localizationMap.Interval } @@ -51,7 +51,7 @@ - + @if (!settings.hideLabels) { widgets.date-range-navigator.localizationMap.Step size } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html index 5c153621fc..de4bc3c1a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html @@ -18,8 +18,8 @@ @if (addMode || editMode) {
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss index 5155866e84..9a895dc7e0 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss @@ -13,18 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../../../../../scss/constants"; + :host { - .predicate-list { - overflow-y: auto; - overflow-x: hidden; - max-height: 350px; - .no-data-found { - height: 50px; - } + .filter-title { + padding: 12px 8px; + font-size: 14px; + font-weight: 500; + } + .no-data-found { + height: 50px; + font-size: 16px; + } + + .key-filter-list-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); } .filters-operation { - margin-top: -18px; - color: #666; - font-weight: 500; + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + position: absolute; + top: -12px; + left: 10px; + background-color: white; + } + &-label { + font-size: 15px; + font-weight: 400; + color: $tb-primary-color; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba($tb-primary-color, 0.32); + background-color: rgba($tb-primary-color, 0.04); + } } } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html index e0c3aa20c7..63554e8034 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html @@ -15,70 +15,75 @@ limitations under the License. --> -
-
-
- @switch (valueType) { - @case (valueTypeEnum.STRING) { - - - - } - @case (valueTypeEnum.NUMERIC) { - - - - } - @case (valueTypeEnum.DATE_TIME) { - +
+
+
+
+ @switch (valueType) { + @case (valueTypeEnum.STRING) { + + {{ hintText | translate }} + + + } + @case (valueTypeEnum.NUMERIC) { + + {{ hintText | translate }} + + + } + @case (valueTypeEnum.DATE_TIME) { + + } + @case (valueTypeEnum.BOOLEAN) { + + {{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }} + + } } - @case (valueTypeEnum.BOOLEAN) { - - {{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }} - - } - } +
-
{{ hintText | translate }}
-
-
-
-
- - - - {{'filter.no-dynamic-value' | translate}} - - @for (sourceType of dynamicValueSourceTypes; track sourceType) { - - {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} +
+
+
+ + filter.dynamic-source-type + + + {{'filter.no-dynamic-value' | translate}} - } - - -
filter.dynamic-source-type
-
-
- - - -
filter.source-attribute
-
- @if (!allow && inheritMode) { -
- - {{ 'filter.inherit-owner' | translate}} - -
filter.source-attribute-not-set
+ @for (sourceType of dynamicValueSourceTypes; track sourceType) { + + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} + + } + + +
+
+ + filter.source-attribute + +
- } + @if (!allow && inheritMode) { +
+ + {{ 'filter.inherit-owner' | translate}} + +
filter.source-attribute-not-set
+
+ } +
+ @if (!onlyUserDynamicSource) { +
} } + @if (type !== filterPredicateType.COMPLEX) { + + + }
@if (type !== filterPredicateType.COMPLEX && displayUserParameters) { - - + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts index e4e327bf0a..797f8f2a4f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts @@ -14,19 +14,34 @@ /// limitations under the License. /// -import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, UntypedFormBuilder, - UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, - Validators } from '@angular/forms'; -import { EntityKeyValueType, FilterPredicateType, KeyFilterPredicateInfo } from '@shared/models/query/query.models'; +import { + EntityKeyValueType, + FilterPredicateType, + KeyFilterPredicateInfo, + BooleanOperation, booleanOperationTranslationMap, + NumericOperation, numericOperationTranslationMap, + StringOperation, stringOperationTranslationMap, ComplexFilterPredicateInfo, KeyFilterPredicateUserInfo, + KeyFilterPredicate +} from '@shared/models/query/query.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models'; +import { + ComplexFilterPredicateDialogComponent +} from '@home/components/filter/complex-filter-predicate-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { + FilterUserInfoDialogComponent, + FilterUserInfoDialogData +} from '@home/components/filter/filter-user-info-dialog.component'; @Component({ selector: 'tb-filter-predicate', @@ -46,7 +61,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; ], standalone: false }) -export class FilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { +export class FilterPredicateComponent implements ControlValueAccessor, Validator { @Input() disabled: boolean; @@ -60,23 +75,35 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator @Input() onlyUserDynamicSource = false; - filterPredicateFormGroup: UntypedFormGroup; + filterPredicateFormGroup = this.fb.group({ + operation: [], + ignoreCase: false, + predicates: [], + value: [], + userInfo: [] + }); type: FilterPredicateType; filterPredicateType = FilterPredicateType; + stringOperations = Object.keys(StringOperation); + stringOperationEnum = StringOperation; + stringOperationTranslations = stringOperationTranslationMap; + + numericOperations = Object.keys(NumericOperation); + numericOperationEnum = NumericOperation; + numericOperationTranslations = numericOperationTranslationMap; + + booleanOperations = Object.keys(BooleanOperation); + booleanOperationEnum = BooleanOperation; + booleanOperationTranslations = booleanOperationTranslationMap; + private propagateChange = null; constructor(private fb: UntypedFormBuilder, - private destroyRef: DestroyRef) { - } - - ngOnInit(): void { - this.filterPredicateFormGroup = this.fb.group({ - predicate: [null, [Validators.required]], - userInfo: [null, []] - }); + private destroyRef: DestroyRef, + private dialog: MatDialog) { this.filterPredicateFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { @@ -93,7 +120,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; - if (this.disabled) { + if (isDisabled) { this.filterPredicateFormGroup.disable({emitEvent: false}); } else { this.filterPredicateFormGroup.enable({emitEvent: false}); @@ -108,19 +135,83 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator writeValue(predicate: KeyFilterPredicateInfo): void { this.type = predicate.keyFilterPredicate.type; - this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false}); + this.filterPredicateFormGroup.patchValue(predicate.keyFilterPredicate, {emitEvent: false}); this.filterPredicateFormGroup.get('userInfo').patchValue(predicate.userInfo, {emitEvent: false}); } private updateModel() { let predicate: KeyFilterPredicateInfo = null; if (this.filterPredicateFormGroup.valid) { + const v = this.filterPredicateFormGroup.getRawValue(); + let keyFilterPredicate: KeyFilterPredicate; + if (this.type === FilterPredicateType.COMPLEX) { + keyFilterPredicate = { + type: FilterPredicateType.COMPLEX, + operation: v.operation, + predicates: v.predicates + } as KeyFilterPredicate; + } else { + keyFilterPredicate = { + type: this.type, + value: v.value, + operation: v.operation, + ignoreCase: v.ignoreCase + } as KeyFilterPredicate; + } predicate = { - keyFilterPredicate: this.filterPredicateFormGroup.getRawValue().predicate, - userInfo: this.filterPredicateFormGroup.getRawValue().userInfo + keyFilterPredicate, + userInfo: v.userInfo }; } this.propagateChange(predicate); } + public openComplexFilterDialog() { + this.dialog.open(ComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: { + type: FilterPredicateType.COMPLEX, + operation: this.filterPredicateFormGroup.get('operation').value, + predicates: this.filterPredicateFormGroup.get('predicates').value + }, + readonly: this.disabled, + valueType: this.valueType, + isAdd: false, + key: this.key, + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource, + onlyUserDynamicSource: this.onlyUserDynamicSource + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filterPredicateFormGroup.patchValue(result); + } + } + ); + } + + public openFilterUserInfoDialog() { + this.dialog.open(FilterUserInfoDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + keyFilterPredicateUserInfo: this.filterPredicateFormGroup.get('userInfo').value, + valueType: this.valueType, + key: this.key, + operation: this.filterPredicateFormGroup.get('operation').value, + readonly: this.disabled + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filterPredicateFormGroup.get('userInfo').patchValue(result); + } + } + ); + } } diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html index 59c4c47eea..8fdbaab47e 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html @@ -25,72 +25,61 @@ close - @if (isLoading$ | async) { - - - } - @if ((isLoading$ | async) === false) { -
- }
-
- -
-
filter.filter
-
filter.editable
-
+
+
+ + + filter.filter + + {{ control.get('filter').value }} + + + + filter.editable + + + + + + + +
+ + + +
+
+
+ + +
+
+
+ filter.no-filters
-
- - @for (filterControl of filtersFormArray().controls; track filterControl) { -
- {{$index + 1}}. -
- {{filterControl.get('filter').value}} -
- - -
- - - -
-
- } -
-
-
-
-
+
+
{{ 'filter.key-filter' | translate }}
+
+
- - filter.key-type.key-type - - @for (type of entityKeyTypes; track type) { - - {{entityKeyTypeTranslations.get(type) | translate}} - + formGroupName="key"> + + filter.key-type.key-type + + @for (type of entityKeyTypes; track type) { + + {{entityKeyTypeTranslations.get(type) | translate}} + + } + + + + filter.key-name + + @if (keyFilterFormGroup.get('key.key').value && showAutocomplete) { + } - - - - filter.key-name - - @if (keyFilterFormGroup.get('key.key').value && showAutocomplete) { - - } - - @for (keyName of filteredKeysName | async; track keyName) { - - + + @for (keyName of filteredKeysName | async; track keyName) { + + + + } + + @if (keyFilterFormGroup.get('key.key').hasError('required')) { + + {{ 'filter.key-name-required' | translate }} + + } + +
+ + filter.value-type.value-type + + + + {{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }} + + @for (valueType of entityKeyValueTypesKeys; track valueType) { + + + {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} } - - @if (keyFilterFormGroup.get('key.key').hasError('required')) { + + @if (keyFilterFormGroup.get('valueType').hasError('required')) { - {{ 'filter.key-name-required' | translate }} + {{ 'filter.value-type-required' | translate }} } -
- - filter.value-type.value-type - - - - {{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }} - - @for (valueType of entityKeyValueTypesKeys; track valueType) { - - - {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} - - } - - @if (keyFilterFormGroup.get('valueType').hasError('required')) { - - {{ 'filter.value-type-required' | translate }} - - } - - @if (isConstantKeyType) { -
-
- @switch (keyFilterFormGroup.get('valueType').value) { - @case (entityKeyValueTypeEnum.STRING) { - - filter.value - - - } - @case (entityKeyValueTypeEnum.NUMERIC) { - - filter.value - - - } - @case (entityKeyValueTypeEnum.DATE_TIME) { - - } - @case (entityKeyValueTypeEnum.BOOLEAN) { - - {{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} - + @if (isConstantKeyType) { +
+
+ @switch (keyFilterFormGroup.get('valueType').value) { + @case (entityKeyValueTypeEnum.STRING) { + + filter.value + + + } + @case (entityKeyValueTypeEnum.NUMERIC) { + + filter.value + + + } + @case (entityKeyValueTypeEnum.DATE_TIME) { + + } + @case (entityKeyValueTypeEnum.BOOLEAN) { + + {{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} + + } } - } +
-
- } + } +
@if (keyFilterFormGroup.get('valueType').value) { - - +
+
+
{{ 'filter.filters' | translate }} +
+
+ + +
} -
+
+ @if (MapProvider.custom === layerFormGroup.get('provider').value) { +
+ +
+ {{ 'widgets.maps.layer.provider.custom.vector-tiles' | translate }} +
+
+
+ }
-
{{ (MapProvider.custom === layerFormGroup.get('provider').value ? 'widgets.maps.layer.provider.custom.tile-url' : 'widgets.maps.layer.layer') | translate }}
+
{{ (MapProvider.custom === layerFormGroup.get('provider').value ? (layerFormGroup.get('vectorTiles').value ? 'widgets.maps.layer.provider.custom.style-url' : 'widgets.maps.layer.provider.custom.tile-url') : 'widgets.maps.layer.layer') | translate }}
+ @if (MapProvider.openfreemap === layerFormGroup.get('provider').value) { + + + @for (styleType of openFreeMapStyleTypes; track styleType) { + + {{ openFreeMapStyleTranslationMap.get(styleType) | translate }} + + } + + + } @if (MapProvider.openstreet === layerFormGroup.get('provider').value) { @@ -117,6 +138,14 @@
+ @if (MapProvider.custom === layerFormGroup.get('provider').value) { +
+
widgets.maps.layer.provider.custom.attribution
+ + + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html index d3a0ba01d4..6c99a7bfcd 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html @@ -78,6 +78,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts index 797f8f2a4f..0b074c2205 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts @@ -155,7 +155,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator type: this.type, value: v.value, operation: v.operation, - ignoreCase: v.ignoreCase + ignoreCase: !!v.ignoreCase } as KeyFilterPredicate; } predicate = { From b15c8fd52c1417e6b3fdfa5aa5c80bb4cab7a030 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 9 Apr 2026 14:13:52 +0200 Subject: [PATCH 253/313] Added validators --- .../filter/filter-predicate.component.ts | 15 +++++++++++++++ .../filter/filters-dialog.component.html | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts index 0b074c2205..1311176976 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts @@ -22,6 +22,7 @@ import { NG_VALUE_ACCESSOR, ValidationErrors, Validator, + Validators, } from '@angular/forms'; import { EntityKeyValueType, @@ -135,10 +136,24 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator writeValue(predicate: KeyFilterPredicateInfo): void { this.type = predicate.keyFilterPredicate.type; + this.updateValidators(); this.filterPredicateFormGroup.patchValue(predicate.keyFilterPredicate, {emitEvent: false}); this.filterPredicateFormGroup.get('userInfo').patchValue(predicate.userInfo, {emitEvent: false}); } + private updateValidators(): void { + const operationCtrl = this.filterPredicateFormGroup.get('operation'); + const predicatesCtrl = this.filterPredicateFormGroup.get('predicates'); + operationCtrl.setValidators([Validators.required]); + if (this.type === FilterPredicateType.COMPLEX) { + predicatesCtrl.setValidators([Validators.required]); + } else { + predicatesCtrl.clearValidators(); + } + operationCtrl.updateValueAndValidity({emitEvent: false}); + predicatesCtrl.updateValueAndValidity({emitEvent: false}); + } + private updateModel() { let predicate: KeyFilterPredicateInfo = null; if (this.filterPredicateFormGroup.valid) { diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html index 8fdbaab47e..452045d164 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html @@ -41,7 +41,7 @@ - +
From 055db10939e671a8635b5363db756f6a0f05a21b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 9 Apr 2026 14:24:09 +0200 Subject: [PATCH 254/313] Moved dashboard filters to new style (#15394) * Moved dashboard filters to new style * Clean up * Fixed table alignment * Added validators --- ...lex-filter-predicate-dialog.component.html | 23 +- .../filter-predicate-list.component.html | 112 ++++------ .../filter-predicate-list.component.scss | 42 +++- .../filter-predicate-value.component.html | 121 ++++++----- .../filter/filter-predicate.component.html | 101 ++++++--- .../filter/filter-predicate.component.ts | 142 ++++++++++-- .../filter/filters-dialog.component.html | 111 +++++----- .../filter/filters-dialog.component.scss | 42 +--- .../filter/filters-dialog.component.ts | 2 + .../filter/key-filter-dialog.component.html | 205 +++++++++--------- 10 files changed, 513 insertions(+), 388 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html index 59d5faa155..cc86556a26 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html @@ -26,17 +26,16 @@
-
- - filter.operation.operation - - @for (operation of complexOperations; track operation) { - - {{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} - - } - - +
+
+
{{ 'filter.filters' | translate }} +
+ + {{ complexOperationTranslations.get(complexOperationEnum.AND) | translate }} + {{ complexOperationTranslations.get(complexOperationEnum.OR) | translate }} + +
-
+
+
+
+ + + +
-
- } - filter.no-filters + } +
-
- -
-
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss index 5155866e84..9a895dc7e0 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.scss @@ -13,18 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../../../../../scss/constants"; + :host { - .predicate-list { - overflow-y: auto; - overflow-x: hidden; - max-height: 350px; - .no-data-found { - height: 50px; - } + .filter-title { + padding: 12px 8px; + font-size: 14px; + font-weight: 500; + } + .no-data-found { + height: 50px; + font-size: 16px; + } + + .key-filter-list-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); } .filters-operation { - margin-top: -18px; - color: #666; - font-weight: 500; + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + position: absolute; + top: -12px; + left: 10px; + background-color: white; + } + &-label { + font-size: 15px; + font-weight: 400; + color: $tb-primary-color; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba($tb-primary-color, 0.32); + background-color: rgba($tb-primary-color, 0.04); + } } } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html index e0c3aa20c7..63554e8034 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html @@ -15,70 +15,75 @@ limitations under the License. --> -
-
-
- @switch (valueType) { - @case (valueTypeEnum.STRING) { - - - - } - @case (valueTypeEnum.NUMERIC) { - - - - } - @case (valueTypeEnum.DATE_TIME) { - +
+
+
+
+ @switch (valueType) { + @case (valueTypeEnum.STRING) { + + {{ hintText | translate }} + + + } + @case (valueTypeEnum.NUMERIC) { + + {{ hintText | translate }} + + + } + @case (valueTypeEnum.DATE_TIME) { + + } + @case (valueTypeEnum.BOOLEAN) { + + {{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }} + + } } - @case (valueTypeEnum.BOOLEAN) { - - {{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }} - - } - } +
-
{{ hintText | translate }}
-
-
-
-
- - - - {{'filter.no-dynamic-value' | translate}} - - @for (sourceType of dynamicValueSourceTypes; track sourceType) { - - {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} +
+
+
+ + filter.dynamic-source-type + + + {{'filter.no-dynamic-value' | translate}} - } - - -
filter.dynamic-source-type
-
-
- - - -
filter.source-attribute
-
- @if (!allow && inheritMode) { -
- - {{ 'filter.inherit-owner' | translate}} - -
filter.source-attribute-not-set
+ @for (sourceType of dynamicValueSourceTypes; track sourceType) { + + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} + + } + + +
+
+ + filter.source-attribute + +
- } + @if (!allow && inheritMode) { +
+ + {{ 'filter.inherit-owner' | translate}} + +
filter.source-attribute-not-set
+
+ } +
+ @if (!onlyUserDynamicSource) { +
} } + @if (type !== filterPredicateType.COMPLEX) { + + + }
@if (type !== filterPredicateType.COMPLEX && displayUserParameters) { - - + }
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts index e4e327bf0a..1311176976 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts @@ -14,19 +14,35 @@ /// limitations under the License. /// -import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, UntypedFormBuilder, - UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator, - Validators + Validators, } from '@angular/forms'; -import { EntityKeyValueType, FilterPredicateType, KeyFilterPredicateInfo } from '@shared/models/query/query.models'; +import { + EntityKeyValueType, + FilterPredicateType, + KeyFilterPredicateInfo, + BooleanOperation, booleanOperationTranslationMap, + NumericOperation, numericOperationTranslationMap, + StringOperation, stringOperationTranslationMap, ComplexFilterPredicateInfo, KeyFilterPredicateUserInfo, + KeyFilterPredicate +} from '@shared/models/query/query.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models'; +import { + ComplexFilterPredicateDialogComponent +} from '@home/components/filter/complex-filter-predicate-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { + FilterUserInfoDialogComponent, + FilterUserInfoDialogData +} from '@home/components/filter/filter-user-info-dialog.component'; @Component({ selector: 'tb-filter-predicate', @@ -46,7 +62,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; ], standalone: false }) -export class FilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { +export class FilterPredicateComponent implements ControlValueAccessor, Validator { @Input() disabled: boolean; @@ -60,23 +76,35 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator @Input() onlyUserDynamicSource = false; - filterPredicateFormGroup: UntypedFormGroup; + filterPredicateFormGroup = this.fb.group({ + operation: [], + ignoreCase: false, + predicates: [], + value: [], + userInfo: [] + }); type: FilterPredicateType; filterPredicateType = FilterPredicateType; + stringOperations = Object.keys(StringOperation); + stringOperationEnum = StringOperation; + stringOperationTranslations = stringOperationTranslationMap; + + numericOperations = Object.keys(NumericOperation); + numericOperationEnum = NumericOperation; + numericOperationTranslations = numericOperationTranslationMap; + + booleanOperations = Object.keys(BooleanOperation); + booleanOperationEnum = BooleanOperation; + booleanOperationTranslations = booleanOperationTranslationMap; + private propagateChange = null; constructor(private fb: UntypedFormBuilder, - private destroyRef: DestroyRef) { - } - - ngOnInit(): void { - this.filterPredicateFormGroup = this.fb.group({ - predicate: [null, [Validators.required]], - userInfo: [null, []] - }); + private destroyRef: DestroyRef, + private dialog: MatDialog) { this.filterPredicateFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { @@ -93,7 +121,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; - if (this.disabled) { + if (isDisabled) { this.filterPredicateFormGroup.disable({emitEvent: false}); } else { this.filterPredicateFormGroup.enable({emitEvent: false}); @@ -108,19 +136,97 @@ export class FilterPredicateComponent implements ControlValueAccessor, Validator writeValue(predicate: KeyFilterPredicateInfo): void { this.type = predicate.keyFilterPredicate.type; - this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false}); + this.updateValidators(); + this.filterPredicateFormGroup.patchValue(predicate.keyFilterPredicate, {emitEvent: false}); this.filterPredicateFormGroup.get('userInfo').patchValue(predicate.userInfo, {emitEvent: false}); } + private updateValidators(): void { + const operationCtrl = this.filterPredicateFormGroup.get('operation'); + const predicatesCtrl = this.filterPredicateFormGroup.get('predicates'); + operationCtrl.setValidators([Validators.required]); + if (this.type === FilterPredicateType.COMPLEX) { + predicatesCtrl.setValidators([Validators.required]); + } else { + predicatesCtrl.clearValidators(); + } + operationCtrl.updateValueAndValidity({emitEvent: false}); + predicatesCtrl.updateValueAndValidity({emitEvent: false}); + } + private updateModel() { let predicate: KeyFilterPredicateInfo = null; if (this.filterPredicateFormGroup.valid) { + const v = this.filterPredicateFormGroup.getRawValue(); + let keyFilterPredicate: KeyFilterPredicate; + if (this.type === FilterPredicateType.COMPLEX) { + keyFilterPredicate = { + type: FilterPredicateType.COMPLEX, + operation: v.operation, + predicates: v.predicates + } as KeyFilterPredicate; + } else { + keyFilterPredicate = { + type: this.type, + value: v.value, + operation: v.operation, + ignoreCase: !!v.ignoreCase + } as KeyFilterPredicate; + } predicate = { - keyFilterPredicate: this.filterPredicateFormGroup.getRawValue().predicate, - userInfo: this.filterPredicateFormGroup.getRawValue().userInfo + keyFilterPredicate, + userInfo: v.userInfo }; } this.propagateChange(predicate); } + public openComplexFilterDialog() { + this.dialog.open(ComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: { + type: FilterPredicateType.COMPLEX, + operation: this.filterPredicateFormGroup.get('operation').value, + predicates: this.filterPredicateFormGroup.get('predicates').value + }, + readonly: this.disabled, + valueType: this.valueType, + isAdd: false, + key: this.key, + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource, + onlyUserDynamicSource: this.onlyUserDynamicSource + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filterPredicateFormGroup.patchValue(result); + } + } + ); + } + + public openFilterUserInfoDialog() { + this.dialog.open(FilterUserInfoDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + keyFilterPredicateUserInfo: this.filterPredicateFormGroup.get('userInfo').value, + valueType: this.valueType, + key: this.key, + operation: this.filterPredicateFormGroup.get('operation').value, + readonly: this.disabled + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filterPredicateFormGroup.get('userInfo').patchValue(result); + } + } + ); + } } diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html index 59c4c47eea..452045d164 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html @@ -25,72 +25,61 @@ close - @if (isLoading$ | async) { - - - } - @if ((isLoading$ | async) === false) { -
- }
-
- -
-
filter.filter
-
filter.editable
-
+
+
+ + + filter.filter + + {{ control.get('filter').value }} + + + + filter.editable + + + + + + + +
+ + + +
+
+
+ + +
+
+
+ filter.no-filters
-
- - @for (filterControl of filtersFormArray().controls; track filterControl) { -
- {{$index + 1}}. -
- {{filterControl.get('filter').value}} -
- - -
- - - -
-
- } -
-
-
-
-
+
+
{{ 'filter.key-filter' | translate }}
+
+
- - filter.key-type.key-type - - @for (type of entityKeyTypes; track type) { - - {{entityKeyTypeTranslations.get(type) | translate}} - + formGroupName="key"> + + filter.key-type.key-type + + @for (type of entityKeyTypes; track type) { + + {{entityKeyTypeTranslations.get(type) | translate}} + + } + + + + filter.key-name + + @if (keyFilterFormGroup.get('key.key').value && showAutocomplete) { + } - - - - filter.key-name - - @if (keyFilterFormGroup.get('key.key').value && showAutocomplete) { - - } - - @for (keyName of filteredKeysName | async; track keyName) { - - + + @for (keyName of filteredKeysName | async; track keyName) { + + + + } + + @if (keyFilterFormGroup.get('key.key').hasError('required')) { + + {{ 'filter.key-name-required' | translate }} + + } + +
+ + filter.value-type.value-type + + + + {{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }} + + @for (valueType of entityKeyValueTypesKeys; track valueType) { + + + {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} } - - @if (keyFilterFormGroup.get('key.key').hasError('required')) { + + @if (keyFilterFormGroup.get('valueType').hasError('required')) { - {{ 'filter.key-name-required' | translate }} + {{ 'filter.value-type-required' | translate }} } -
- - filter.value-type.value-type - - - - {{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }} - - @for (valueType of entityKeyValueTypesKeys; track valueType) { - - - {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} - - } - - @if (keyFilterFormGroup.get('valueType').hasError('required')) { - - {{ 'filter.value-type-required' | translate }} - - } - - @if (isConstantKeyType) { -
-
- @switch (keyFilterFormGroup.get('valueType').value) { - @case (entityKeyValueTypeEnum.STRING) { - - filter.value - - - } - @case (entityKeyValueTypeEnum.NUMERIC) { - - filter.value - - - } - @case (entityKeyValueTypeEnum.DATE_TIME) { - - } - @case (entityKeyValueTypeEnum.BOOLEAN) { - - {{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} - + @if (isConstantKeyType) { +
+
+ @switch (keyFilterFormGroup.get('valueType').value) { + @case (entityKeyValueTypeEnum.STRING) { + + filter.value + + + } + @case (entityKeyValueTypeEnum.NUMERIC) { + + filter.value + + + } + @case (entityKeyValueTypeEnum.DATE_TIME) { + + } + @case (entityKeyValueTypeEnum.BOOLEAN) { + + {{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} + + } } - } +
-
- } + } +
@if (keyFilterFormGroup.get('valueType').value) { - - +
+
+
{{ 'filter.filters' | translate }} +
+
+ + +
} -
+
+ @if (provisionConfigurationFormGroup.get('provisionDeviceKey').hasError('required')) { + {{ 'device-profile.provision-device-key-required' | translate }} + } + + + device-profile.provision-device-secret + + + @if (provisionConfigurationFormGroup.get('provisionDeviceSecret').hasError('required')) { + {{ 'device-profile.provision-device-secret-required' | translate }} + } + + +
- -
- - device-profile.provision-device-key - - - @if (provisionConfigurationFormGroup.get('provisionDeviceKey').hasError('required')) { - {{ 'device-profile.provision-device-key-required' | translate }} - } - - - device-profile.provision-device-secret - - - @if (provisionConfigurationFormGroup.get('provisionDeviceSecret').hasError('required')) { - {{ 'device-profile.provision-device-secret-required' | translate }} - } - -
-
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 4065dc602d..05aef4cfbb 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -62,7 +62,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu provisionConfigurationFormGroup: UntypedFormGroup; deviceProvisionType = DeviceProvisionType; - deviceProvisionTypes = Object.keys(DeviceProvisionType); + deviceProvisionTypes = Object.values(DeviceProvisionType); deviceProvisionTypeTranslateMap = deviceProvisionTypeTranslationMap; private requiredValue: boolean; @@ -213,8 +213,4 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup.get('provisionDeviceKey').reset({value: null, disabled: true}, {emitEvent: false}); } } - - getDeviceProvisionType(type: string): string { - return this.deviceProvisionTypeTranslateMap.get(type as DeviceProvisionType); - } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html index e2ac4da12b..1ad031c3a9 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html @@ -25,7 +25,7 @@
- + @for (attributeName of attributeNames; track attributeName) { } - + @if (nameValueControl.get('value').hasError('required')) { @@ -68,8 +68,7 @@
} -
device-profile.lwm2m.no-attributes-set
+
device-profile.lwm2m.no-attributes-set
@if (!disabled && isAddEnabled) {
- - - - } - @if (!embedded) { - + } - + + + + @if (isEdit) { +
+ {{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }} +
+ } +
+ + @if (!readonly && (hideToolbar || widgetEditMode)) { +
+ + + +
+ } + @if (!embedded) { + + } + Date: Tue, 9 Jun 2026 15:28:31 +0300 Subject: [PATCH 295/313] Improve dashboard page html template formatting --- .../dashboard-page.component.html | 320 +++++++++--------- 1 file changed, 158 insertions(+), 162 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 5b9c3c6340..d91a15c3dd 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -116,168 +116,164 @@ edit - } - @if (isEdit) { - - - - - - } - @if (currentDashboardId && isEdit && isTenantAdmin()) { - - - } - @if (isEdit) { - - - } - @if (isEdit) { - - - } - @if (isEdit) { - - - } - - @if (isEdit) { - - - - - } @else { - @if (displayDashboardTimewindow()) { - > - - > - - } - } - @if (isEdit) { - - - } - - - - - @if (!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()) { - - - } -
-
- + } + @if (isEdit) { + + + + + + } + @if (currentDashboardId && isEdit && isTenantAdmin()) { + + + } + @if (isEdit) { + + + + + + + } + + @if (isEdit) { + + + + + } @else { + @if (displayDashboardTimewindow()) { + > + + > + + } + } + @if (isEdit) { + + + } + + + + + @if (!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()) { + + + } +
+
+
Date: Tue, 9 Jun 2026 16:08:41 +0300 Subject: [PATCH 296/313] Improve dashboard page html template formatting --- .../dashboard-page.component.html | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index d91a15c3dd..c62c29555e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -114,8 +114,8 @@ matTooltip="{{'dashboard.edit-mode' | translate}}" matTooltipPosition="below"> edit - + } @if (isEdit) {
@@ -276,12 +275,12 @@
+ tb-toast toastTarget="dashboardRoot" + #dashboardContainer + [class.is-fullscreen]="forceFullscreen" + [class.tb-dashboard-toolbar-opened]="toolbarOpened" + [class.tb-dashboard-toolbar-animated]="isToolbarOpenedAnimate" + [class.tb-dashboard-toolbar-closed]="!toolbarOpened"> @if (!widgetEditMode) { @@ -297,7 +296,7 @@
} + [class.tb-shrinked]="isEditingWidget"> @if (layouts.right.show) { Date: Tue, 9 Jun 2026 18:29:35 +0300 Subject: [PATCH 297/313] test(device-connectivity): pass DockerComposeParams to gateway compose renderer Update DeviceConnectivityUtilTest to match the new getGatewayDockerComposeFile signature: build a DockerComposeParams (true, "tb-gateway", true, true, true, true) and pass it alongside the host, image tag, and credentials. --- .../server/dao/util/DeviceConnectivityUtilTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java b/dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java index 2c6fee2fcf..e53a76f09f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java @@ -20,6 +20,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.device.DockerComposeParams; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -115,8 +116,9 @@ class DeviceConnectivityUtilTest { } private static String renderCompose(DeviceCredentials credentials) throws Exception { + DockerComposeParams params = new DockerComposeParams(true, "tb-gateway", true, true, true, true); var resource = DeviceConnectivityUtil.getGatewayDockerComposeFile( - "host.docker.internal", "3.8-stable", credentials); + "host.docker.internal", "3.8-stable", credentials, params); try (var in = resource.getInputStream()) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } From 4bfc810d75eedd1b8b620582f050ea6b4970620c Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 10 Jun 2026 15:40:25 +0300 Subject: [PATCH 298/313] sparkplug-unique-device-names - comments -2 --- .../transport/DefaultTransportApiService.java | 38 +++++------ .../AbstractMqttV5ClientSparkplugTest.java | 67 ++++++++++--------- ...ientSparkplugBAttributesInProfileTest.java | 2 +- .../MqttV5ClientSparkplugBAttributesTest.java | 2 +- ...gBConnectionDevicesCreatingBeforeTest.java | 3 +- .../MqttV5ClientSparkplugBConnectionTest.java | 2 +- .../sparkplug/rpc/MqttV5RpcSparkplugTest.java | 2 +- .../MqttV5ClientSparkplugBTelemetryTest.java | 2 +- .../AbstractGatewaySessionHandler.java | 8 ++- .../session/SparkplugNodeSessionHandler.java | 35 +++++----- 10 files changed, 81 insertions(+), 80 deletions(-) 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 ed43c71027..2fe4543806 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 @@ -396,7 +396,6 @@ public class DefaultTransportApiService implements TransportApiService { // Security check: verify that the device was created by this gateway boolean isRelated = false; try { - // Security check: verify that the device was originally created by this gateway isRelated = relationService.checkRelation( gateway.getTenantId(), gateway.getId(), @@ -405,37 +404,34 @@ public class DefaultTransportApiService implements TransportApiService { RelationTypeGroup.COMMON ); } catch (Exception e) { - // Log the error from the relation service but return null to allow potential recovery - log.error("[{}] Error checking relation for device {}", gateway.getId(), existingDevice.getId(), e); - return null; + log.error("[{}] Failed checking relation for device {}", + gateway.getId(), + existingDevice.getId(), + e); + throw new RuntimeException( + "Failed checking relation for device " + existingDevice.getId(), + e + ); } - // If the device is found but not related to this gateway, it's a security breach + // If the device is found but not related to this gateway if (!isRelated) { - log.error("[{}] Security breach attempt! Gateway tried to rename device [{}] without 'Created' relation.", - gateway.getId(), existingDevice.getId()); - // Throwing exception to halt the entire connection process - throw new RuntimeException("Security breach attempt! Unauthorized device rename."); + log.debug("[{}] Device [{}] exists but is not related to gateway. " + + "Skipping rename and allowing creation of Sparkplug device [{}].", + gateway.getId(), + existingDevice.getId(), + requestMsg.getDeviceName()); + + return null; } // Logic for renaming the device if it's related and no naming conflicts exist boolean changed = false; String newName = requestMsg.getDeviceName(); - if (!newName.equals(existingDevice.getName())) { - // Check if the new name is already taken by another device - Device conflictDevice = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), newName); - - if (conflictDevice != null) { - log.warn("[{}] Cannot rename device [{}] to [{}]: name already exists!", - gateway.getId(), existingDevice.getId(), newName); - return existingDevice; - } - existingDevice.setName(newName); - // Update label only if it's empty to avoid overwriting user changes - if (existingDevice.getLabel() == null || existingDevice.getLabel().isEmpty()) { + if (StringUtils.isEmpty(existingDevice.getLabel())) { existingDevice.setLabel(deviceId); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java index 2adc7b08a5..c3c72e916c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java @@ -31,7 +31,6 @@ import org.junit.Assert; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -58,6 +57,7 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import static java.util.concurrent.Executors.newFixedThreadPool; import static org.awaitility.Awaitility.await; import static org.eclipse.paho.mqttv5.common.packet.MqttWireMessage.MESSAGE_TYPE_CONNACK; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -108,7 +108,8 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte protected static final String metricBirthName_Int32 = "Device Metric int32"; protected Set sparkplugAttributesMetricNames; - public void beforeSparkplugTest(boolean isCreateDevices) throws Exception { + public void beforeSparkplugTest() throws Exception { + MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() .gatewayName(edgeNodeDeviceName) .isSparkplug(true) @@ -116,24 +117,26 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte .transportPayloadType(TransportPayloadType.PROTOBUF) .build(); processBeforeTest(configProperties); - if (isCreateDevices) { - // 1. Create the first device with a short name (legacy style) - String deviceName1 = deviceId + "_1"; - Device device1 = createDevice(deviceName1, deviceProfile.getName(), false); - - // 2. Establish 'Created' relation so the transport identifies this gateway as the owner - String relationType = "Created"; - EntityRelation relation1 = createFromRelation(savedGateway, device1, relationType); - doPost("/api/relation", relation1).andExpect(status().isOk()); - - // 3. Create the second device with a full-path name - String deviceName2 = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + deviceId + "_2"; - Device device2 = createDevice(deviceName2, deviceProfile.getName(), false); - - // 4. Establish 'Created' relation for the second device as well - EntityRelation relation2 = createFromRelation(savedGateway, device2, relationType); - doPost("/api/relation", relation2).andExpect(status().isOk()); - } + } + + public void seedLegacyAndFullPathDevices() throws Exception { + // 1. Create the first device with a short name (legacy style) + String deviceName1 = deviceId + "_1"; + Device device1 = createDevice(deviceName1, deviceProfile.getName(), false); + + // 2. Establish 'Created' relation so the transport identifies this gateway as the owner + String relationType = "Created"; + EntityRelation relation1 = createFromRelation(savedGateway, device1, relationType); + doPost("/api/relation", relation1).andExpect(status().isOk()); + + // 3. Create the second device with a full-path name + String deviceName2 = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + deviceId + "_2"; + Device device2 = createDevice(deviceName2, deviceProfile.getName(), false); + + // 4. Establish 'Created' relation for the second device as well + EntityRelation relation2 = createFromRelation(savedGateway, device2, relationType); + doPost("/api/relation", relation2).andExpect(status().isOk()); + } public void clientWithCorrectNodeAccessTokenWithNDEATH() throws Exception { @@ -208,7 +211,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte .setTimestamp(ts) .setSeq(getSeqNum()); String deviceIdName = deviceId + "_" + i; - String deviceName = groupId + ":" + edgeNode + ":" + deviceIdName; + String deviceName = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + deviceIdName; payloadBirthDevice.addMetrics(metric); if (client.isConnected()) { client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + TOPIC_SPLIT_SEPARATOR + SparkplugMessageType.DBIRTH.name() + TOPIC_SPLIT_SEPARATOR + edgeNode + TOPIC_SPLIT_SEPARATOR + deviceIdName, @@ -304,7 +307,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte protected void renameCollisionWhenTargetNameAlreadyExists_Test() throws Exception { long ts = calendar.getTimeInMillis(); String shortName = deviceId + "_1"; // Created in beforeTest - String fullPathName = groupId + ":" + edgeNode + ":" + shortName; + String fullPathName = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + shortName; // Manually create a device that already has the "new" full-path name to trigger a collision createDevice(fullPathName, deviceProfile.getName(), false); @@ -318,7 +321,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte // Gateway sends DBIRTH for the short name. // Transport will try to rename it but should find a conflict and handle it gracefully. - client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/DBIRTH/" + edgeNode + "/" + shortName, + client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + "/DBIRTH/" + edgeNode + TOPIC_SPLIT_SEPARATOR + shortName, payload.build().toByteArray(), 0, false); await("Checking stability after collision") @@ -352,10 +355,10 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte .setTimestamp(ts).setSeq(getSeqNum()); // 2. Unauthorized gateway attempts to rename this device via Sparkplug topic path - client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/DBIRTH/" + edgeNode + "/" + strangerName, + client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + "/DBIRTH/" + edgeNode + TOPIC_SPLIT_SEPARATOR + strangerName, payload.build().toByteArray(), 0, false); - String expectedFullPath = groupId + ":" + edgeNode + ":" + strangerName; + String expectedFullPath = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + strangerName; // 3. Verify security: the original device must still be linked to its short name with the same ID await("Verify original device was not hijacked") @@ -394,10 +397,10 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte .setTimestamp(ts).setSeq(getSeqNum()); // Unauthorized gateway attempts to rename the device via Sparkplug topic - client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/DBIRTH/" + edgeNode + "/" + strangerName, + client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + "/DBIRTH/" + edgeNode + TOPIC_SPLIT_SEPARATOR + strangerName, payload.build().toByteArray(), 0, false); - String expectedFullPath = groupId + ":" + edgeNode + ":" + strangerName; + String expectedFullPath = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + strangerName; await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> doGet("/api/tenant/devices?deviceName=" + expectedFullPath, Device.class, status().isNotFound()) ); @@ -438,7 +441,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte String concurrentDeviceName = "concurrent_device"; clientWithCorrectNodeAccessTokenWithNDEATH(); - java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(threadCount); + java.util.concurrent.ExecutorService executor = newFixedThreadPool(threadCount); long ts = calendar.getTimeInMillis(); for (int i = 0; i < threadCount; i++) { @@ -446,7 +449,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte try { SparkplugBProto.Payload.Builder payload = SparkplugBProto.Payload.newBuilder() .setTimestamp(ts).setSeq(0); - client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/DBIRTH/" + edgeNode + "/" + concurrentDeviceName, + client.publish(TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_SEPARATOR + groupId + "/DBIRTH/" + edgeNode + TOPIC_SPLIT_SEPARATOR + concurrentDeviceName, payload.build().toByteArray(), 0, false); } catch (Exception e) { log.error("Concurrent publish failed", e); @@ -454,9 +457,9 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte }); } - String expectedName = groupId + ":" + edgeNode + ":" + concurrentDeviceName; + String expectedName = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + concurrentDeviceName; await("Wait for concurrent registration result") - .atMost(40, TimeUnit.SECONDS) // Restored to 40s as requested + .atMost(40, TimeUnit.SECONDS) .until(() -> doGet("/api/tenant/devices?deviceName=" + expectedName, Device.class) != null); executor.shutdown(); @@ -506,7 +509,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte .setTimestamp(ts) .setSeq(getSeqNum()); String deviceIdName = deviceId + "_1"; - String deviceName = groupId + ":" + edgeNode + ":" + deviceIdName; + String deviceName = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + deviceIdName; payloadBirthDevice.addMetrics(metric); if (client.isConnected()) { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java index 8cd713ad7d..37abade6e7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java @@ -33,7 +33,7 @@ public class MqttV5ClientSparkplugBAttributesInProfileTest extends AbstractMqttV public void beforeTest() throws Exception { sparkplugAttributesMetricNames = new HashSet<>(); sparkplugAttributesMetricNames.add(metricBirthName_Int32); - beforeSparkplugTest(false); + beforeSparkplugTest(); } @After diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java index b080cfbff0..3826b59bdc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java @@ -29,7 +29,7 @@ public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSp @Before public void beforeTest() throws Exception { - beforeSparkplugTest(false); + beforeSparkplugTest(); } @After diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionDevicesCreatingBeforeTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionDevicesCreatingBeforeTest.java index 92583e655c..08fc4ea9a5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionDevicesCreatingBeforeTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionDevicesCreatingBeforeTest.java @@ -34,7 +34,8 @@ public class MqttV5ClientSparkplugBConnectionDevicesCreatingBeforeTest extends A */ @Before public void beforeTest() throws Exception { - beforeSparkplugTest(true); + beforeSparkplugTest(); + seedLegacyAndFullPathDevices(); } @After diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java index 0d5e73e2f0..01038c21f1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java @@ -29,7 +29,7 @@ public class MqttV5ClientSparkplugBConnectionTest extends AbstractMqttV5ClientSp @Before public void beforeTest() throws Exception { - beforeSparkplugTest(false); + beforeSparkplugTest(); } @After diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java index 08ade0274d..e90f6e94da 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java @@ -28,7 +28,7 @@ public class MqttV5RpcSparkplugTest extends AbstractMqttV5RpcSparkplugTest { @Before public void beforeTest() throws Exception { - beforeSparkplugTest(false); + beforeSparkplugTest(); } @After diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java index 5d9f453415..bafe2d81d2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java @@ -29,7 +29,7 @@ public class MqttV5ClientSparkplugBTelemetryTest extends AbstractMqttV5ClientSpa @Before public void beforeTest() throws Exception { - beforeSparkplugTest(false); + beforeSparkplugTest(); } @After diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 94c24f5a1d..99f10eb176 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -261,7 +261,7 @@ public abstract class AbstractGatewaySessionHandler { ack(msg, MqttReasonCodes.PubAck.SUCCESS); log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName); @@ -277,6 +277,10 @@ public abstract class AbstractGatewaySessionHandler onDeviceConnect(String deviceName, String deviceType) { + return onDeviceConnect(deviceName, deviceType, false); + } + ListenableFuture onDeviceConnect(String deviceName, String deviceType, boolean isSparkplug) { T result = devices.get(deviceName); if (result == null) { @@ -902,7 +906,7 @@ public abstract class AbstractGatewaySessionHandler onSuccess, Consumer onFailure) { - ListenableFuture deviceCtxFuture = onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE, false); + ListenableFuture deviceCtxFuture = onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE); process(deviceCtxFuture, onSuccess, onFailure); } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index 6cb566d445..1a088344c8 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -115,26 +115,23 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler deviceCtx = this.onDeviceConnectProto(topic); - String finalDeviceName = deviceName; - contextListenableFuture = Futures.transform(deviceCtx, ctx -> { - if (topic.isType(DBIRTH)) { - sendSparkplugStateOnTelemetry(ctx.getSessionInfo(), finalDeviceName, ONLINE, - sparkplugBProto.getTimestamp()); - try { - ctx.setDeviceBirthMetrics(sparkplugBProto.getMetricsList()); - } catch (IllegalArgumentException | DuplicateKeyException e) { - log.error("[{}] Failed to set birth metrics", finalDeviceName, e); - throw new RuntimeException(e); - } + deviceName = checkDeviceName(topic.getNodeDeviceNameAllPath()); + ListenableFuture deviceCtx = this.onDeviceConnectProto(topic); + String finalDeviceName = deviceName; + + contextListenableFuture = Futures.transform(deviceCtx, ctx -> { + if (topic.isType(DBIRTH)) { + sendSparkplugStateOnTelemetry(ctx.getSessionInfo(), finalDeviceName, ONLINE, + sparkplugBProto.getTimestamp()); + try { + ctx.setDeviceBirthMetrics(sparkplugBProto.getMetricsList()); + } catch (IllegalArgumentException | DuplicateKeyException e) { + log.error("[{}] Failed to set birth metrics", finalDeviceName, e); + throw new RuntimeException(e); } - return ctx; - }, MoreExecutors.directExecutor()); - } catch (IllegalArgumentException | DuplicateKeyException e) { - throw new RuntimeException(e); - } + } + return ctx; + }, MoreExecutors.directExecutor()); } Set attributesMetricNames = ((MqttDeviceProfileTransportConfiguration) deviceSessionCtx .getDeviceProfile().getProfileData().getTransportConfiguration()).getSparkplugAttributesMetricNames(); From b6d1e25c6550b9775a39db75ef3d2c75f74a8474 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Thu, 11 Jun 2026 07:51:06 +0300 Subject: [PATCH 299/313] sparkplug-unique-device-names - comments -3 --- .../mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java index c3c72e916c..949d837573 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java @@ -402,7 +402,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte String expectedFullPath = groupId + DEVICE_NAME_SPLIT_SEPARATOR + edgeNode + DEVICE_NAME_SPLIT_SEPARATOR + strangerName; await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> - doGet("/api/tenant/devices?deviceName=" + expectedFullPath, Device.class, status().isNotFound()) + doGet("/api/tenant/devices?deviceName=" + expectedFullPath, Device.class, status().isOk()) ); } From dfbd49cb3dfade3df66af63d586def6929991c23 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Thu, 11 Jun 2026 11:30:22 +0300 Subject: [PATCH 300/313] sparkplug-unique-device-names - comments -4 --- .../transport/DefaultTransportApiService.java | 8 +++--- .../AbstractMqttV5ClientSparkplugTest.java | 28 +++++++++++++------ 2 files changed, 23 insertions(+), 13 deletions(-) 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 2fe4543806..8977f4023a 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 @@ -400,7 +400,7 @@ public class DefaultTransportApiService implements TransportApiService { gateway.getTenantId(), gateway.getId(), existingDevice.getId(), - "Created", + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON ); } catch (Exception e) { @@ -448,8 +448,8 @@ public class DefaultTransportApiService implements TransportApiService { Device device = new Device(); device.setTenantId(tenantId); device.setName(requestMsg.getDeviceName()); - if (requestMsg.getIsSparkplug()) { - if (topicPath.length == 3) device.setLabel(topicPath[2]); + if (requestMsg.getIsSparkplug() && topicPath.length == 3) { + device.setLabel(topicPath[2]); } device.setType(requestMsg.getDeviceType()); device.setCustomerId(gateway.getCustomerId()); @@ -462,7 +462,7 @@ public class DefaultTransportApiService implements TransportApiService { device = deviceService.saveDevice(device); relationService.saveRelation( tenantId, - new EntityRelation(gateway.getId(), device.getId(), "Created") + new EntityRelation(gateway.getId(), device.getId(), EntityRelation.CONTAINS_TYPE) ); return device; } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java index 949d837573..b318e550d1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java @@ -53,6 +53,7 @@ import java.util.Calendar; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -60,6 +61,8 @@ import java.util.concurrent.atomic.AtomicReference; import static java.util.concurrent.Executors.newFixedThreadPool; import static org.awaitility.Awaitility.await; import static org.eclipse.paho.mqttv5.common.packet.MqttWireMessage.MESSAGE_TYPE_CONNACK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.common.util.JacksonUtil.newArrayNode; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Bytes; @@ -153,9 +156,9 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte public void clientWithCorrectNodeAccessTokenWithNDEATH(long ts, long value) throws Exception { IMqttToken connectionResult = clientMqttV5ConnectWithNDEATH(ts, value, -1L); MqttWireMessage response = connectionResult.getResponse(); - Assert.assertEquals(MESSAGE_TYPE_CONNACK, response.getType()); + assertEquals(MESSAGE_TYPE_CONNACK, response.getType()); MqttConnAck connAckMsg = (MqttConnAck) response; - Assert.assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode()); + assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode()); } public IMqttToken clientMqttV5ConnectWithNDEATH(long ts, long value, Long alias, String... nameSpaceBad) throws Exception { @@ -228,7 +231,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte } } - Assert.assertEquals(cntDevices, devices.size()); + assertEquals(cntDevices, devices.size()); return devices; } @@ -293,10 +296,10 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte return device2.get() != null; }); devices.add(device2.get()); - Assert.assertEquals(cntDevices, devices.size()); + assertEquals(cntDevices, devices.size()); state_ONLINE_ALL (devices, calendar.getTimeInMillis()); // Without full topic: as it was in the old version. When deviceId is updated to full theme, Label is also updated to old deviceId - Assert.assertEquals(deviceIdNameLabel1, device1.get().getLabel()); + assertEquals(deviceIdNameLabel1, device1.get().getLabel()); // // With a full topic: if new. When creating a device by a client to a full topic, if the Label was not filled in - we do not touch it. Assert.assertNull(device2.get().getLabel()); } @@ -367,8 +370,8 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte .untilAsserted(() -> { // Check if the original device still exists with its original ID Device currentStranger = doGet("/api/tenant/devices?deviceName=" + strangerName, Device.class); - Assert.assertNotNull("Original device disappeared!", currentStranger); - Assert.assertEquals("Security breach: Original device ID changed!", originalStrangerId, currentStranger.getId()); + assertNotNull("Original device disappeared!", currentStranger); + assertEquals("Security breach: Original device ID changed!", originalStrangerId, currentStranger.getId()); // Even if the gateway created a NEW device with a full path, it must have a different ID Device newDevice = doGet("/api/tenant/devices?deviceName=" + expectedFullPath, Device.class); @@ -390,6 +393,8 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte stranger.setName(strangerName); stranger.setType("default"); doPost("/api/device", stranger); + Device originalStrangerDevice = + doGet("/api/tenant/devices?deviceName=" + strangerName, Device.class); clientWithCorrectNodeAccessTokenWithNDEATH(); @@ -404,6 +409,11 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> doGet("/api/tenant/devices?deviceName=" + expectedFullPath, Device.class, status().isOk()) ); + + Device strangerDevice = + doGet("/api/tenant/devices?deviceName=" + strangerName, Device.class); + assertNotNull(strangerDevice); + assertEquals(originalStrangerDevice.getId(), strangerDevice.getId()); } protected void state_ONLINE_ALL (List devices, long ts) { @@ -441,7 +451,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte String concurrentDeviceName = "concurrent_device"; clientWithCorrectNodeAccessTokenWithNDEATH(); - java.util.concurrent.ExecutorService executor = newFixedThreadPool(threadCount); + ExecutorService executor = newFixedThreadPool(threadCount); long ts = calendar.getTimeInMillis(); for (int i = 0; i < threadCount; i++) { @@ -526,7 +536,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte devices.add(device.get()); } - Assert.assertEquals(1, devices.size()); + assertEquals(1, devices.size()); return devices; } From c256f3c685dad0215644ffc7d00a42516733d20c Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Thu, 11 Jun 2026 12:17:46 +0300 Subject: [PATCH 301/313] sparkplug-unique-device-names - comments -5 --- .../service/transport/DefaultTransportApiService.java | 5 +++-- .../mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) 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 8977f4023a..b617a4931f 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 @@ -141,6 +141,7 @@ public class DefaultTransportApiService implements TransportApiService { private final OtaPackageService otaPackageService; private final OtaPackageDataCache otaPackageDataCache; private final QueueService queueService; + public static final String GATEWAY_CREATED_RELATION = "Created"; private final ConcurrentMap deviceCreationLocks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); @@ -400,7 +401,7 @@ public class DefaultTransportApiService implements TransportApiService { gateway.getTenantId(), gateway.getId(), existingDevice.getId(), - EntityRelation.CONTAINS_TYPE, + GATEWAY_CREATED_RELATION, RelationTypeGroup.COMMON ); } catch (Exception e) { @@ -462,7 +463,7 @@ public class DefaultTransportApiService implements TransportApiService { device = deviceService.saveDevice(device); relationService.saveRelation( tenantId, - new EntityRelation(gateway.getId(), device.getId(), EntityRelation.CONTAINS_TYPE) + new EntityRelation(gateway.getId(), device.getId(), GATEWAY_CREATED_RELATION) ); return device; } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java index b318e550d1..68237a1839 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java @@ -65,6 +65,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.common.util.JacksonUtil.newArrayNode; +import static org.thingsboard.server.service.transport.DefaultTransportApiService.GATEWAY_CREATED_RELATION; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Bytes; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int16; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int32; @@ -128,8 +129,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte Device device1 = createDevice(deviceName1, deviceProfile.getName(), false); // 2. Establish 'Created' relation so the transport identifies this gateway as the owner - String relationType = "Created"; - EntityRelation relation1 = createFromRelation(savedGateway, device1, relationType); + EntityRelation relation1 = createFromRelation(savedGateway, device1, GATEWAY_CREATED_RELATION); doPost("/api/relation", relation1).andExpect(status().isOk()); // 3. Create the second device with a full-path name @@ -137,7 +137,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte Device device2 = createDevice(deviceName2, deviceProfile.getName(), false); // 4. Establish 'Created' relation for the second device as well - EntityRelation relation2 = createFromRelation(savedGateway, device2, relationType); + EntityRelation relation2 = createFromRelation(savedGateway, device2, GATEWAY_CREATED_RELATION); doPost("/api/relation", relation2).andExpect(status().isOk()); } From f3f76496c61bc16d745d1abc78abecaf90eeac1d Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Thu, 11 Jun 2026 13:02:53 +0300 Subject: [PATCH 302/313] sparkplug-unique-device-names - comments -6 --- .../transport/DefaultTransportApiService.java | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) 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 b617a4931f..5c21096862 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 @@ -396,24 +396,13 @@ public class DefaultTransportApiService implements TransportApiService { // Security check: verify that the device was created by this gateway boolean isRelated = false; - try { - isRelated = relationService.checkRelation( - gateway.getTenantId(), - gateway.getId(), - existingDevice.getId(), - GATEWAY_CREATED_RELATION, - RelationTypeGroup.COMMON - ); - } catch (Exception e) { - log.error("[{}] Failed checking relation for device {}", - gateway.getId(), - existingDevice.getId(), - e); - throw new RuntimeException( - "Failed checking relation for device " + existingDevice.getId(), - e - ); - } + isRelated = relationService.checkRelation( + gateway.getTenantId(), + gateway.getId(), + existingDevice.getId(), + GATEWAY_CREATED_RELATION, + RelationTypeGroup.COMMON + ); // If the device is found but not related to this gateway if (!isRelated) { From b2e82b7c880f35e686964fe8ba8c2b54a5f1f545 Mon Sep 17 00:00:00 2001 From: bruak Date: Thu, 11 Jun 2026 14:42:54 +0300 Subject: [PATCH 303/313] Fix typos: 'resereved' -> 'reserved' and fix protobuf comment grammar --- .../server/common/data/security/DeviceCredentialsFilter.java | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java index 201615eb98..6d7188733e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java @@ -16,7 +16,7 @@ package org.thingsboard.server.common.data.security; /** - * TODO: This is a temporary name. DeviceCredentialsId is resereved in dao layer + * TODO: This is a temporary name. DeviceCredentialsId is reserved in dao layer */ public interface DeviceCredentialsFilter { diff --git a/pom.xml b/pom.xml index 70dbf9f43e..01e1dfe9ee 100755 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ 2.0.1 5.6.0 3.9.5 - 3.25.5 + 3.25.5 1.76.0 1.2.10 1.18.46 From 131ba74514db43ad037c0239adc06de97b35fa47 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 11 Jun 2026 16:29:37 +0300 Subject: [PATCH 304/313] Add an opt-in readinessCheck gate so the edge consumer pauses polling instead of polling and dropping events while not ready (sync/high-priority/disconnected). --- .../KafkaBasedEdgeGrpcSessionManager.java | 32 ++- .../common/consumer/QueueConsumerManager.java | 31 ++- .../consumer/QueueConsumerManagerTest.java | 251 ++++++++++++++++++ 3 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 common/queue/src/test/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManagerTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java index ebc5531320..01174423c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java @@ -35,7 +35,6 @@ import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbKafkaComponent; - import org.thingsboard.server.service.edge.rpc.EdgeSessionState; import org.thingsboard.server.service.edge.rpc.processor.PostgresGeneralEdgeEventsDispatcher; import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; @@ -143,8 +142,7 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan } } catch (Exception e) { log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, edgeId, e); - } - finally { + } finally { initLock.unlock(); } }, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); @@ -176,7 +174,7 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan } private boolean initAndLaunchConsumer(TenantId tenantId, EdgeId edgeId, EdgeSessionState state) { - if (!state.isConnected() || state.isSyncInProgress() || isHighPriorityProcessing) { + if (!isSessionReady(state)) { log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer", tenantId, edgeId, state.isConnected(), state.isSyncInProgress(), isHighPriorityProcessing); return false; @@ -205,6 +203,7 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edgeId)) .consumerExecutor(consumerExecutor) .threadPrefix("edge-events-" + edgeId) + .readinessCheck(this::isReadyToProcessGeneralEvents) .build(); consumer.subscribe(); consumer.launch(); @@ -224,7 +223,9 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan EdgeId edgeId = state.getEdgeId(); log.trace("[{}][{}] starting processing edge events", tenantId, edgeId); - if (!state.isConnected() || state.isSyncInProgress() || isHighPriorityProcessing) { + // Defensive backstop: the loop already gates polling on readiness; this only fires on the narrow race + // where readiness flips during poll(), and that already-polled batch is dropped here (can't rewind). + if (!isSessionReady(state)) { log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " + "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration", tenantId, edgeId, state.isConnected(), state.isSyncInProgress(), isHighPriorityProcessing); @@ -250,6 +251,26 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan } } + /** + * Readiness gate for the edge-event consumer (see {@link QueueConsumerManager}'s {@code readinessCheck}): it polls + * only while the session is connected and no sync or high-priority processing is running. + *

+ * Pausing polling in those windows is deliberate. A batch that is polled but then skipped advances the Kafka + * position without committing, so it is lost until a rebalance; keeping events queued instead matches the no-loss + * behaviour the Postgres-based manager gets by re-reading by seqId. This covers the common case - a batch already + * in flight when a sync starts is a known residual (closing it would need seek/rewind, which {@code TbQueueConsumer} + * lacks). If a sync ever outran {@code max.poll.interval.ms} (default 5 min) the consumer is rebalanced and resumes + * from the committed offset: still no loss, just a possible replay. Edge syncs are seconds, so this is acceptable. + */ + private boolean isReadyToProcessGeneralEvents() { + return isSessionReady(getState()); + } + + /** Single source of truth for "the session may process general edge events" - see {@link #isReadyToProcessGeneralEvents}. */ + private boolean isSessionReady(EdgeSessionState state) { + return state != null && state.isConnected() && !state.isSyncInProgress() && !isHighPriorityProcessing; + } + private void cancelMigrationAndProcessingInit() { EdgeSessionState state = getState(); log.trace("[{}] cancelling edge migration & processing init for edge", state != null ? state.getEdgeId() : "unknown"); @@ -299,4 +320,5 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan Thread.currentThread().interrupt(); } } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java index 4adc0354c4..9ee6bc0b50 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java @@ -30,6 +30,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.BooleanSupplier; import java.util.function.Supplier; @Slf4j @@ -40,6 +41,8 @@ public class QueueConsumerManager { private final long pollInterval; private final ExecutorService consumerExecutor; private final String threadPrefix; + /** Optional poll gate: while {@code false} the loop skips polling so the position doesn't advance; {@code null} = always ready (default). */ + private final BooleanSupplier readinessCheck; @Getter private final TbQueueConsumer consumer; @@ -49,12 +52,13 @@ public class QueueConsumerManager { @Builder public QueueConsumerManager(String name, MsgPackProcessor msgPackProcessor, long pollInterval, Supplier> consumerCreator, - ExecutorService consumerExecutor, String threadPrefix) { + ExecutorService consumerExecutor, String threadPrefix, BooleanSupplier readinessCheck) { this.name = name; this.pollInterval = pollInterval; this.msgPackProcessor = msgPackProcessor; this.consumerExecutor = consumerExecutor; this.threadPrefix = threadPrefix; + this.readinessCheck = readinessCheck; this.consumer = consumerCreator.get(); } @@ -84,6 +88,12 @@ public class QueueConsumerManager { private void consumerLoop(TbQueueConsumer consumer) { while (!stopped && !consumer.isStopped()) { try { + if (!isReadyToProcess()) { + if (!awaitNextReadinessCheck()) { + return; + } + continue; + } List msgs = consumer.poll(pollInterval); if (msgs.isEmpty()) { continue; @@ -102,6 +112,25 @@ public class QueueConsumerManager { } } + private boolean isReadyToProcess() { + return readinessCheck == null || readinessCheck.getAsBoolean(); + } + + /** + * Waits one poll interval before readiness is re-checked. Returns {@code false} if interrupted, which is treated as + * a stop signal so the consumer loop exits. + */ + private boolean awaitNextReadinessCheck() { + log.trace("[{}] Consumer is not ready to process messages yet, skipping poll iteration", name); + try { + Thread.sleep(pollInterval); + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + public void stop() { log.debug("[{}] Stopping consumer", name); stopped = true; diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManagerTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManagerTest.java new file mode 100644 index 0000000000..c987fb502e --- /dev/null +++ b/common/queue/src/test/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManagerTest.java @@ -0,0 +1,251 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.common.consumer; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsg; + +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; + +@Slf4j +class QueueConsumerManagerTest { + + private static final long POLL_INTERVAL_MS = 20L; + // Before asserting the consumer never polled we wait until the loop has evaluated the readiness gate at least + // this many times. That proves the consumer thread is actually running and deciding not to poll, rather than the + // assertion passing vacuously because the thread simply has not started yet. + private static final int MIN_READINESS_CHECKS = 3; + + private final AtomicBoolean readyToProcess = new AtomicBoolean(false); + private final AtomicInteger readinessChecks = new AtomicInteger(); + private final TestQueueConsumer consumer = new TestQueueConsumer(); + private ExecutorService consumerExecutor; + private QueueConsumerManager manager; + + @AfterEach + void tearDown() { + if (manager != null) { + manager.stop(); + } + if (consumerExecutor != null) { + consumerExecutor.shutdownNow(); + } + } + + @Test + void eventQueuedWhileNotReadyIsDeliveredAfterReadinessGateOpensInsteadOfBeingDropped() { + List delivered = new CopyOnWriteArrayList<>(); + + consumer.enqueue(List.of(mock(TbQueueMsg.class))); + + // The processor is unconditional: only the readiness gate may hold the event back, so delivery proves the + // gate (not the processor) is what kept the event queued while not ready. + manager = launchManager(consumer, countingReadiness(readyToProcess, readinessChecks), (msgs, c) -> { + delivered.addAll(msgs); + c.commit(); + }); + + // The loop is running and repeatedly evaluating the gate during the not-ready (sync) window... + awaitReadinessGateEvaluated(readinessChecks); + // ...yet the queued event is neither polled nor delivered - it stays in the queue rather than being dropped. + assertThat(consumer.getPollCount()) + .as("consumer must not poll while not ready") + .isZero(); + assertThat(delivered) + .as("event must not be delivered while not ready") + .isEmpty(); + + // Sync completes - the processor becomes ready. + readyToProcess.set(true); + + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(delivered) + .as("event queued during the not-ready window must be delivered, not dropped") + .hasSize(1)); + } + + @Test + void consumerIsNotPolledWhileNotReadyToProcess() { + manager = launchManager(consumer, countingReadiness(readyToProcess, readinessChecks), (msgs, c) -> c.commit()); + + awaitReadinessGateEvaluated(readinessChecks); + assertThat(consumer.getPollCount()) + .as("consumer must not be polled while not ready to process") + .isZero(); + + readyToProcess.set(true); + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(consumer.getPollCount()) + .as("consumer resumes polling once ready") + .isPositive()); + } + + @Test + void consumerWithoutReadinessCheckPollsAndDeliversImmediately() { + List delivered = new CopyOnWriteArrayList<>(); + + consumer.enqueue(List.of(mock(TbQueueMsg.class))); + + // No readiness gate configured - the consumer must default to "always ready", preserving the behaviour every + // consumer that does not opt in relies on. + manager = launchManager(consumer, null, (msgs, c) -> { + delivered.addAll(msgs); + c.commit(); + }); + + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(delivered) + .as("consumer without a readiness gate must poll and deliver immediately") + .hasSize(1)); + } + + @Test + void consumerLoopExitsWhenInterruptedWhileNotReady() throws Exception { + manager = launchManager(consumer, countingReadiness(readyToProcess, readinessChecks), (msgs, c) -> c.commit()); + + // The loop is parked in the not-ready wait... + awaitReadinessGateEvaluated(readinessChecks); + + // ...interrupting the worker (as shutdownNow does on stop) must end the loop, not spin or hang. + consumerExecutor.shutdownNow(); + assertThat(consumerExecutor.awaitTermination(5, TimeUnit.SECONDS)) + .as("consumer loop must exit when interrupted while waiting to become ready") + .isTrue(); + } + + private static void awaitReadinessGateEvaluated(AtomicInteger readinessChecks) { + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(readinessChecks.get()) + .as("consumer loop must be running and repeatedly evaluating the readiness gate") + .isGreaterThanOrEqualTo(MIN_READINESS_CHECKS)); + } + + private static BooleanSupplier countingReadiness(AtomicBoolean ready, AtomicInteger readinessChecks) { + return () -> { + readinessChecks.incrementAndGet(); + return ready.get(); + }; + } + + private QueueConsumerManager launchManager(TestQueueConsumer consumer, BooleanSupplier readinessCheck, + QueueConsumerManager.MsgPackProcessor processor) { + consumerExecutor = Executors.newSingleThreadExecutor(); + QueueConsumerManager queueConsumerManager = QueueConsumerManager.builder() + .name("test-consumer") + .pollInterval(POLL_INTERVAL_MS) + .consumerCreator(() -> consumer) + .consumerExecutor(consumerExecutor) + .readinessCheck(readinessCheck) + .msgPackProcessor(processor) + .build(); + queueConsumerManager.subscribe(); + queueConsumerManager.launch(); + return queueConsumerManager; + } + + private static class TestQueueConsumer implements TbQueueConsumer { + + private final Queue> batches = new ConcurrentLinkedQueue<>(); + private final AtomicInteger pollCount = new AtomicInteger(); + private volatile boolean stopped; + + void enqueue(List batch) { + batches.add(batch); + } + + int getPollCount() { + return pollCount.get(); + } + + @Override + public List poll(long durationInMillis) { + pollCount.incrementAndGet(); + List batch = batches.poll(); + if (batch != null) { + return batch; + } + try { + Thread.sleep(durationInMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return Collections.emptyList(); + } + + @Override + public String getTopic() { + return "test-topic"; + } + + @Override + public void subscribe() { + } + + @Override + public void subscribe(Set partitions) { + } + + @Override + public void stop() { + stopped = true; + } + + @Override + public void unsubscribe() { + stopped = true; + } + + @Override + public void commit() { + } + + @Override + public boolean isStopped() { + return stopped; + } + + @Override + public Set getPartitions() { + return Collections.emptySet(); + } + + @Override + public List getFullTopicNames() { + return Collections.emptyList(); + } + + } + +} From 28f25d5331693cfb768fde7a700e084fd3f4aa8c Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 11 Jun 2026 16:29:37 +0300 Subject: [PATCH 305/313] KafkaBasedEdgeGrpcSessionManagerTest: added --- .../KafkaBasedEdgeGrpcSessionManagerTest.java | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManagerTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManagerTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManagerTest.java new file mode 100644 index 0000000000..0759a6b05e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManagerTest.java @@ -0,0 +1,322 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.session.manager; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.discovery.TopicService; +import org.thingsboard.server.queue.kafka.KafkaAdmin; +import org.thingsboard.server.queue.provider.TbCoreQueueFactory; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.rpc.DownlinkMessageMapper; +import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; +import org.thingsboard.server.service.edge.rpc.EdgeSessionState; +import org.thingsboard.server.service.edge.rpc.session.EdgeSession; +import org.thingsboard.server.service.edge.rpc.session.EdgeSessionsHolder; + +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the readiness gate that {@link KafkaBasedEdgeGrpcSessionManager} feeds into its edge-event consumer. + *

+ * The generic poll-gate mechanism is covered by {@code QueueConsumerManagerTest}; these tests pin the edge-specific + * half of the fix: the manager's readiness predicate, and the fact that the consumer is actually wired with it - so + * dropping the {@code .readinessCheck(...)} builder line would silently reintroduce the event-loss bug. + */ +class KafkaBasedEdgeGrpcSessionManagerTest { + + private static final long POLL_INTERVAL_MS = 20L; + + private EdgeContextComponent ctx; + private TbCoreQueueFactory tbCoreQueueFactory; + private EdgeSessionState state; + private KafkaBasedEdgeGrpcSessionManager manager; + + @BeforeEach + void setUp() { + ctx = mock(EdgeContextComponent.class); + tbCoreQueueFactory = mock(TbCoreQueueFactory.class); + TopicService topicService = mock(TopicService.class); + KafkaAdmin kafkaAdmin = mock(KafkaAdmin.class); + EdgeSessionsHolder sessions = mock(EdgeSessionsHolder.class); + + manager = new KafkaBasedEdgeGrpcSessionManager(tbCoreQueueFactory, topicService, kafkaAdmin, sessions); + + Edge edge = new Edge(new EdgeId(UUID.randomUUID())); + edge.setTenantId(TenantId.fromUUID(UUID.randomUUID())); + state = new EdgeSessionState(); + state.setEdge(edge); + + EdgeSession session = mock(EdgeSession.class); + when(session.getState()).thenReturn(state); + + ReflectionTestUtils.setField(manager, "session", session); + ReflectionTestUtils.setField(manager, "ctx", ctx); + ReflectionTestUtils.setField(manager, "downlinkMessageMapper", mock(DownlinkMessageMapper.class)); + } + + @AfterEach + void tearDown() { + if (manager != null) { + manager.destroy(); + } + } + + @Test + void readyOnlyWhenConnectedNotSyncingNotHighPriority() { + setReadiness(true, false, false); + assertThat(isReadyToProcessGeneralEvents()) + .as("connected, not syncing, no high-priority work -> ready") + .isTrue(); + } + + @Test + void notReadyWhenDisconnected() { + setReadiness(false, false, false); + assertThat(isReadyToProcessGeneralEvents()) + .as("disconnected -> not ready") + .isFalse(); + } + + @Test + void notReadyWhileSyncInProgress() { + setReadiness(true, true, false); + assertThat(isReadyToProcessGeneralEvents()) + .as("sync in progress -> not ready (this is the window where events were being dropped)") + .isFalse(); + } + + @Test + void notReadyWhileHighPriorityProcessing() { + setReadiness(true, false, true); + assertThat(isReadyToProcessGeneralEvents()) + .as("high-priority processing -> not ready") + .isFalse(); + } + + @Test + void initConsumerWiresReadinessPredicateIntoConsumerGate() { + stubStorageSettings(); + + @SuppressWarnings("unchecked") + TbQueueConsumer> queueConsumer = mock(TbQueueConsumer.class); + // Report stopped so the launched consumer loop exits immediately - this test asserts on wiring, not polling. + when(queueConsumer.isStopped()).thenReturn(true); + when(tbCoreQueueFactory.createEdgeEventMsgConsumer(any(), any())).thenReturn(queueConsumer); + + setReadiness(true, false, false); + initConsumer(); + + QueueConsumerManager> consumer = manager.getConsumer(); + assertThat(consumer).as("consumer must be built").isNotNull(); + + BooleanSupplier readinessCheck = (BooleanSupplier) ReflectionTestUtils.getField(consumer, "readinessCheck"); + assertThat(readinessCheck) + .as("the edge consumer must be wired with a readinessCheck (the .readinessCheck(...) builder line)") + .isNotNull(); + + // It must be the live predicate, not a snapshot: flipping the session state must flip the gate. + assertThat(readinessCheck.getAsBoolean()).as("ready session -> gate open").isTrue(); + state.tryStartSync(); + assertThat(readinessCheck.getAsBoolean()).as("sync starts -> gate closes, consumer pauses polling").isFalse(); + } + + @Test + void eventArrivingDuringSyncIsHeldByTheEdgeConsumerUntilSyncCompletes() { + stubStorageSettings(); + + RecordingEdgeEventConsumer queueConsumer = new RecordingEdgeEventConsumer(); + when(tbCoreQueueFactory.createEdgeEventMsgConsumer(any(), any())).thenReturn(queueConsumer); + + // The consumer is launched only while the session is ready. + setReadiness(true, false, false); + initConsumer(); + + // Sync starts: the gate closes. Wait until the loop has actually parked on it (poll count stops advancing) + // before enqueuing - otherwise we would race an in-flight poll() and the test would be non-deterministic. + state.tryStartSync(); + awaitParkedOnClosedGate(queueConsumer); + + // An event lands in the edge-event topic during the sync window - exactly the case that used to be dropped. + @SuppressWarnings("unchecked") + TbProtoQueueMsg event = mock(TbProtoQueueMsg.class); + int pollsBeforeEvent = queueConsumer.getPollCount(); + queueConsumer.enqueue(List.of(event)); + + // While sync is in progress the consumer stays parked: it neither polls nor consumes the event. + sleepQuietly(POLL_INTERVAL_MS * 5); + assertThat(queueConsumer.getPolledEvents()) + .as("event must not be polled while sync is in progress (it must stay queued, not be dropped)") + .isEmpty(); + assertThat(queueConsumer.getPollCount()) + .as("consumer must not poll at all while the gate is closed") + .isEqualTo(pollsBeforeEvent); + + // Sync completes: the gate opens and the held event is finally picked up by the consumer. + state.finishSync(); + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(queueConsumer.getPolledEvents()) + .as("event held during sync must be picked up once sync completes, not lost") + .hasSize(1)); + } + + private void stubStorageSettings() { + EdgeEventStorageSettings storageSettings = mock(EdgeEventStorageSettings.class); + when(storageSettings.getNoRecordsSleepInterval()).thenReturn(POLL_INTERVAL_MS); + when(ctx.getEdgeEventStorageSettings()).thenReturn(storageSettings); + } + + private void initConsumer() { + ReflectionTestUtils.invokeMethod(manager, "initConsumerAndExecutor", state.getTenantId(), state.getEdgeId(), state); + } + + private boolean isReadyToProcessGeneralEvents() { + return Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(manager, "isReadyToProcessGeneralEvents")); + } + + private void setReadiness(boolean connected, boolean syncInProgress, boolean highPriorityProcessing) { + state.setConnected(connected); + if (syncInProgress) { + state.tryStartSync(); + } else { + state.finishSync(); + } + ReflectionTestUtils.setField(manager, "isHighPriorityProcessing", highPriorityProcessing); + } + + private static void awaitParkedOnClosedGate(RecordingEdgeEventConsumer consumer) { + await().atMost(5, TimeUnit.SECONDS).until(() -> { + int before = consumer.getPollCount(); + // Several poll intervals with no new poll means the loop is parked on the readiness gate rather than + // blocked inside poll(), so it is safe to enqueue without racing an in-flight read. + sleepQuietly(POLL_INTERVAL_MS * 5); + return consumer.getPollCount() == before; + }); + } + + private static void sleepQuietly(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * Fake edge-event consumer that records what it was actually polled, so a test can prove an event stayed in the + * queue while the readiness gate was closed and was only read once it opened. + */ + private static class RecordingEdgeEventConsumer implements TbQueueConsumer> { + + private final Queue>> pending = new ConcurrentLinkedQueue<>(); + private final List> polledEvents = new CopyOnWriteArrayList<>(); + private final AtomicInteger pollCount = new AtomicInteger(); + private volatile boolean stopped; + + void enqueue(List> batch) { + pending.add(batch); + } + + int getPollCount() { + return pollCount.get(); + } + + List> getPolledEvents() { + return polledEvents; + } + + @Override + public List> poll(long durationInMillis) { + pollCount.incrementAndGet(); + List> batch = pending.poll(); + if (batch != null) { + polledEvents.addAll(batch); + return batch; + } + sleepQuietly(durationInMillis); + return Collections.emptyList(); + } + + @Override + public String getTopic() { + return "test-edge-event-topic"; + } + + @Override + public void subscribe() { + } + + @Override + public void subscribe(Set partitions) { + } + + @Override + public void stop() { + stopped = true; + } + + @Override + public void unsubscribe() { + stopped = true; + } + + @Override + public void commit() { + } + + @Override + public boolean isStopped() { + return stopped; + } + + @Override + public List getFullTopicNames() { + return Collections.emptyList(); + } + + @Override + public Set getPartitions() { + return Collections.emptySet(); + } + + } + +} From 4f8dcb3dceaff18716b6cda93ea0ca8b23b9df17 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 11 Jun 2026 16:43:58 +0300 Subject: [PATCH 306/313] Remove comments --- .../manager/KafkaBasedEdgeGrpcSessionManager.java | 11 ----------- .../manager/KafkaBasedEdgeGrpcSessionManagerTest.java | 7 ------- 2 files changed, 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java index 01174423c2..f42f48421a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManager.java @@ -251,17 +251,6 @@ public class KafkaBasedEdgeGrpcSessionManager extends AbstractEdgeGrpcSessionMan } } - /** - * Readiness gate for the edge-event consumer (see {@link QueueConsumerManager}'s {@code readinessCheck}): it polls - * only while the session is connected and no sync or high-priority processing is running. - *

- * Pausing polling in those windows is deliberate. A batch that is polled but then skipped advances the Kafka - * position without committing, so it is lost until a rebalance; keeping events queued instead matches the no-loss - * behaviour the Postgres-based manager gets by re-reading by seqId. This covers the common case - a batch already - * in flight when a sync starts is a known residual (closing it would need seek/rewind, which {@code TbQueueConsumer} - * lacks). If a sync ever outran {@code max.poll.interval.ms} (default 5 min) the consumer is rebalanced and resumes - * from the committed offset: still no loss, just a possible replay. Edge syncs are seconds, so this is acceptable. - */ private boolean isReadyToProcessGeneralEvents() { return isSessionReady(getState()); } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManagerTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManagerTest.java index 0759a6b05e..7ddc2af192 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/session/manager/KafkaBasedEdgeGrpcSessionManagerTest.java @@ -54,13 +54,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -/** - * Unit tests for the readiness gate that {@link KafkaBasedEdgeGrpcSessionManager} feeds into its edge-event consumer. - *

- * The generic poll-gate mechanism is covered by {@code QueueConsumerManagerTest}; these tests pin the edge-specific - * half of the fix: the manager's readiness predicate, and the fact that the consumer is actually wired with it - so - * dropping the {@code .readinessCheck(...)} builder line would silently reintroduce the event-loss bug. - */ class KafkaBasedEdgeGrpcSessionManagerTest { private static final long POLL_INTERVAL_MS = 20L; From 07af9eaf27fff04665479b135c9438f654877427 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 22 May 2026 11:33:45 +0200 Subject: [PATCH 307/313] Added hint for Unread notifications widget in widget selector --- .../main/data/json/system/widget_types/unread_notifications.json | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/data/json/system/widget_types/unread_notifications.json b/application/src/main/data/json/system/widget_types/unread_notifications.json index 7d699e7018..c4d2f899fb 100644 --- a/application/src/main/data/json/system/widget_types/unread_notifications.json +++ b/application/src/main/data/json/system/widget_types/unread_notifications.json @@ -3,6 +3,7 @@ "name": "Unread notifications", "deprecated": false, "image": "tb-image;/api/images/system/unread_notification_system_widget_image.png", + "description": "Displays the latest unread notifications and alarms for the current user.", "descriptor": { "type": "static", "sizeX": 5.5, From 9728e2c03d6e7a234a945b0c8ca71efb318de4bb Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 16 Jun 2026 15:56:18 +0200 Subject: [PATCH 308/313] Fixed device/asset profile import when default rule chain no longer exists (#15718) * Fixed device/asset profile import when default rule chain no longer exists * Minor fixes * Code refactorings and optimizations --- .../import-export/import-export.service.ts | 40 ++++++++++++++++++- .../assets/locale/locale.constant-en_US.json | 1 + 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 6593e8c9bf..3c4d3b3f12 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -89,12 +89,16 @@ import { import { FormProperty, propertyValid } from '@shared/models/dynamic-form.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedField } from '@shared/models/calculated-field.models'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; export type editMissingAliasesFunction = (widgets: Array, isSingleWidget: boolean, customTitle: string, missingEntityAliases: EntityAliases) => Observable; type SupportEntityResources = 'includeResourcesInExportWidgetTypes' | 'includeResourcesInExportDashboard' | 'includeBundleWidgetsInExport'; +const profileRuleChainFields = ['defaultRuleChainId', 'defaultEdgeRuleChainId'] as const; +type ProfileRuleChainField = typeof profileRuleChainFields[number]; + // @dynamic @Injectable() export class ImportExportService { @@ -749,7 +753,8 @@ export class ImportExportService { type: 'error'})); throw new Error('Invalid device profile file'); } else { - return this.deviceProfileService.saveDeviceProfile(this.prepareImport(deviceProfile)); + return this.clearMissingRuleChainsAndSave(deviceProfile, + profile => this.deviceProfileService.saveDeviceProfile(profile)); } }), catchError(() => of(null)) @@ -776,13 +781,44 @@ export class ImportExportService { type: 'error'})); throw new Error('Invalid asset profile file'); } else { - return this.assetProfileService.saveAssetProfile(this.prepareImport(assetProfile)); + return this.clearMissingRuleChainsAndSave(assetProfile, + profile => this.assetProfileService.saveAssetProfile(profile)); } }), catchError(() => of(null)) ); } + private clearMissingRuleChainsAndSave & Partial>>( + profile: T, save: (profile: T) => Observable): Observable { + return this.clearMissingProfileRuleChains(profile).pipe( + mergeMap(cleared => save(this.prepareImport(cleared))) + ); + } + + private clearMissingProfileRuleChains>>(profile: T): Observable { + const fieldsToCheck = profileRuleChainFields.filter(field => profile[field]?.id); + + if (!fieldsToCheck.length) { + return of(profile); + } + const ruleChainIds = fieldsToCheck.map(field => profile[field].id); + return this.ruleChainService.getRuleChainsByIds(ruleChainIds, {ignoreErrors: true, ignoreLoading: true}).pipe( + map(ruleChains => { + const existingIds = new Set(ruleChains.map(ruleChain => ruleChain.id.id)); + const missingFields = fieldsToCheck.filter(field => !existingIds.has(profile[field].id)); + if (!missingFields.length) { + return profile; + } + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('rulechain.rulechain-not-found-warning'), + type: 'warn'})); + missingFields.forEach(field => profile[field] = null); + return profile; + }) + ); + } + public exportTenantProfile(tenantProfileId: string) { this.tenantProfileService.getTenantProfile(tenantProfileId).subscribe({ next: (tenantProfile) => { 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 764676dfbc..09c1e1939e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5137,6 +5137,7 @@ "create-new-rulechain": "Create new rule chain", "rulechain-file": "Rule chain file", "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", + "rulechain-not-found-warning": "Default rule chain not found. The profile was saved without one — you can assign it in the profile settings.", "copyId": "Copy rule chain Id", "idCopiedMessage": "Rule chain Id has been copied to clipboard", "select-rulechain": "Select rule chain", From f19019ec024dc9bf29ae8c010665c4350e722c43 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 19 Jun 2026 15:50:38 +0300 Subject: [PATCH 309/313] Address review: run dormant 4.3.1.3 data migration explicitly as the 4.4 upgrade step --- .../service/install/update/DefaultDataUpdateService.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 464714fde7..086d76a25c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -31,6 +31,7 @@ import org.thingsboard.server.service.component.RuleNodeClassInfo; import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; import org.thingsboard.server.service.install.DbUpgradeExecutorService; import org.thingsboard.server.service.install.lts.LtsMigrationService; +import org.thingsboard.server.service.install.lts.V4_3_1_3Migration; import org.thingsboard.server.utils.TbNodeUpgradeUtils; import java.util.ArrayList; @@ -51,11 +52,18 @@ public class DefaultDataUpdateService implements DataUpdateService { private final DbUpgradeExecutorService executorService; private final DatabaseSchemaSettingsService schemaSettingsService; private final LtsMigrationService ltsMigrationService; + private final V4_3_1_3Migration v4313Migration; @Override public void updateData() throws Exception { log.info("Updating data ..."); ltsMigrationService.runDataMigrations(schemaSettingsService.getDbSchemaVersion(), schemaSettingsService.getPackageSchemaVersion()); + // 4.4-release data step: the 4.3.1.3 data migration (obsolete widget-bundle cleanup) is dormant on 4.4 because + // LtsMigrationService.select() filters out non-target-family migrations and there are no 4.4-family beans, so the + // 4.3-family V4_3_1_3Migration is never selected. The 4.4 SUPPORTED_VERSIONS_FOR_UPGRADE allows upgrading from + // sources below 4.3.1.3, so we run it explicitly for every supported 4.3.x -> 4.4 upgrade. It is idempotent + // (bundle deletion checks existence first), so re-running it for a source already at 4.3.1.3 is a safe no-op. + v4313Migration.apply(); log.info("Data updated."); } From c24a52fcc87319f9e96ba29c75e78c4914db7624 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 19 Jun 2026 15:46:58 +0200 Subject: [PATCH 310/313] Fixed JavaScript modules with special characters corrupted when uploading files (#15498) * Fixed JavaScript modules with special characters not displayed correctly in the editor * Optimized code * Fixed approach * Fixed base64toObj * Fixed * Types fix * Extracted shared bytesToString helper, dropped FileReader spoofing --- ui-ngx/src/app/core/utils.ts | 21 ++++++-- .../shared/components/file-input.component.ts | 48 +++++++++++-------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 7f13f08ba6..17458c908a 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -226,8 +226,24 @@ export function objToBase64(obj: any): string { })); } +const textDecoderUtf8 = new TextDecoder('UTF-8', { fatal: true }); +const textDecoderLatin1 = new TextDecoder('ISO-8859-1'); + +export function bytesToString(bytes: Uint8Array): string { + try { + return textDecoderUtf8.decode(bytes); + } catch { + return textDecoderLatin1.decode(bytes); + } +} + export function base64toString(b64Encoded: string): string { - return decodeURIComponent(atob(b64Encoded).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')); + const binary = atob(b64Encoded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytesToString(bytes); } export function objToBase64URI(obj: any): string { @@ -235,8 +251,7 @@ export function objToBase64URI(obj: any): string { } export function base64toObj(b64Encoded: string): any { - const json = decodeURIComponent(atob(b64Encoded).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')); - return JSON.parse(json); + return JSON.parse(base64toString(b64Encoded)); } export function stringToBase64(value: string): string { diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index 727b1628dc..d9f891c7f4 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -38,6 +38,7 @@ import { UtilsService } from '@core/services/utils.service'; import { DialogService } from '@core/services/dialog.service'; import { FileSizePipe } from '@shared/pipe/file-size.pipe'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { bytesToString } from '@core/utils'; @Component({ selector: 'tb-file-input', @@ -216,34 +217,39 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, return; } - const reader = new FileReader(); - reader.onload = () => { - let fileName = null; - let fileContent = null; - let mediaType = null; - if (reader.readyState === reader.DONE) { - fileContent = reader.result; - if (fileContent && fileContent.length > 0) { - if (this.contentConvertFunction) { - fileContent = this.contentConvertFunction(fileContent); - } - fileName = fileContent ? file.name : null; - mediaType = file?.file?.type || null; - } - } - resolve({fileContent, fileName, files: null, mediaType}); - }; - reader.onerror = () => { - resolve({fileContent: null, fileName: null, files: null, mediaType: null}); - }; + const onReadError = () => resolve({fileContent: null, fileName: null, files: null, mediaType: null}); + if (this.readAsBinary) { + const reader = new FileReader(); + reader.onload = () => { + const content = reader.readyState === reader.DONE ? reader.result : null; + resolve(this.buildResult(file, content)); + }; + reader.onerror = onReadError; reader.readAsBinaryString(file.file); } else { - reader.readAsText(file.file); + file.file.arrayBuffer().then( + buf => resolve(this.buildResult(file, bytesToString(new Uint8Array(buf)))), + onReadError + ); } }); } + private buildResult(file: flowjs.FlowFile, content: string | ArrayBuffer): any { + let fileContent: any = content; + let fileName = null; + let mediaType = null; + if (fileContent && fileContent.length > 0) { + if (this.contentConvertFunction) { + fileContent = this.contentConvertFunction(fileContent); + } + fileName = fileContent ? file.name : null; + mediaType = file?.file?.type || null; + } + return {fileContent, fileName, files: null, mediaType}; + } + private checkMaxSize(file: flowjs.FlowFile): boolean { return !this.maxSizeByte || file.size <= this.maxSizeByte; } From 2b635c674d82503a9a2bbd8208ec6dfab32d6f3f Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 22 Jun 2026 11:00:40 +0200 Subject: [PATCH 311/313] Chart Widgets: zoom interaction now syncs the active time window (#15624) * Chart Widgets: zoom interaction now syncs the active time window * Fix for comparison mode * Minor fixes * Minor improvements * Minor code fixes --- ...rt-with-labels-basic-config.component.html | 5 ++ ...hart-with-labels-basic-config.component.ts | 11 ++- ...e-series-chart-basic-config.component.html | 5 ++ ...ime-series-chart-basic-config.component.ts | 13 +++- .../bar-chart-with-labels-widget.component.ts | 1 + .../bar-chart-with-labels-widget.models.ts | 3 + .../widget/lib/chart/echarts-widget.models.ts | 6 ++ .../chart/time-series-chart-tooltip.models.ts | 4 +- .../time-series-chart-widget.component.ts | 1 + .../lib/chart/time-series-chart.models.ts | 2 + .../widget/lib/chart/time-series-chart.ts | 76 ++++++++++++++++++- ...with-labels-widget-settings.component.html | 5 ++ ...t-with-labels-widget-settings.component.ts | 10 ++- ...eries-chart-widget-settings.component.html | 5 ++ ...-series-chart-widget-settings.component.ts | 10 ++- .../widget/widget-container.component.html | 2 +- .../widget/widget-container.component.ts | 5 ++ .../home/models/widget-component.models.ts | 2 +- .../assets/locale/locale.constant-en_US.json | 2 + 19 files changed, 157 insertions(+), 11 deletions(-) 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 f927340691..61398972c4 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 @@ -98,6 +98,11 @@ {{ 'widgets.time-series-chart.data-zoom' | translate }}

+
+ + {{ 'widgets.time-series-chart.data-zoom-update-timewindow' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts index d569bcf37b..bfd430ad36 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts @@ -116,6 +116,7 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom iconColor: [configData.config.iconColor, []], dataZoom: [settings.dataZoom, []], + dataZoomUpdateTimewindow: [settings.dataZoomUpdateTimewindow, []], showBarLabel: [settings.showBarLabel, []], barLabelFont: [settings.barLabelFont, []], @@ -193,6 +194,7 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; this.widgetConfig.config.settings.dataZoom = config.dataZoom; + this.widgetConfig.config.settings.dataZoomUpdateTimewindow = config.dataZoomUpdateTimewindow; this.widgetConfig.config.settings.showBarLabel = config.showBarLabel; this.widgetConfig.config.settings.barLabelFont = config.barLabelFont; @@ -248,10 +250,11 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom } protected validatorTriggers(): string[] { - return ['showTitle', 'showIcon', 'showBarLabel', 'showBarValue', 'showBarBorder', 'showLegend', 'showTooltip', 'tooltipShowDate']; + return ['dataZoom', 'showTitle', 'showIcon', 'showBarLabel', 'showBarValue', 'showBarBorder', 'showLegend', 'showTooltip', 'tooltipShowDate']; } protected updateValidators(emitEvent: boolean, trigger?: string) { + const dataZoom: boolean = this.barChartWidgetConfigForm.get('dataZoom').value; const showTitle: boolean = this.barChartWidgetConfigForm.get('showTitle').value; const showIcon: boolean = this.barChartWidgetConfigForm.get('showIcon').value; const showBarLabel: boolean = this.barChartWidgetConfigForm.get('showBarLabel').value; @@ -261,6 +264,12 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom const showTooltip: boolean = this.barChartWidgetConfigForm.get('showTooltip').value; const tooltipShowDate: boolean = this.barChartWidgetConfigForm.get('tooltipShowDate').value; + if (dataZoom) { + this.barChartWidgetConfigForm.get('dataZoomUpdateTimewindow').enable(); + } else { + this.barChartWidgetConfigForm.get('dataZoomUpdateTimewindow').disable(); + } + if (showTitle) { this.barChartWidgetConfigForm.get('title').enable(); this.barChartWidgetConfigForm.get('titleFont').enable(); 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 0602e55fc9..fcba48e87a 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 @@ -180,6 +180,11 @@ {{ 'widgets.time-series-chart.data-zoom' | translate }}
+
+ + {{ 'widgets.time-series-chart.data-zoom-update-timewindow' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 5b1410e5d7..143e7a682e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -96,7 +96,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon seriesMode = 'series'; predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -164,6 +164,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon iconColor: [configData.config.iconColor, []], dataZoom: [settings.dataZoom, []], + dataZoomUpdateTimewindow: [settings.dataZoomUpdateTimewindow, []], stack: [settings.stack, []], grid: [settings.grid, []], @@ -241,6 +242,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.thresholds = config.thresholds; this.widgetConfig.config.settings.dataZoom = config.dataZoom; + this.widgetConfig.config.settings.dataZoomUpdateTimewindow = config.dataZoomUpdateTimewindow; this.widgetConfig.config.settings.stack = config.stack; this.widgetConfig.config.settings.grid = config.grid; @@ -292,11 +294,12 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } protected validatorTriggers(): string[] { - return ['comparisonEnabled', 'showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate', 'stack']; + return ['comparisonEnabled', 'dataZoom', 'showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate', 'stack']; } protected updateValidators(emitEvent: boolean, trigger?: string) { const comparisonEnabled: boolean = this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').value; + const dataZoom: boolean = this.timeSeriesChartWidgetConfigForm.get('dataZoom').value; const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value; const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value; const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value; @@ -304,6 +307,12 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value; const stack: boolean = this.timeSeriesChartWidgetConfigForm.get('stack').value; + if (dataZoom) { + this.timeSeriesChartWidgetConfigForm.get('dataZoomUpdateTimewindow').enable(); + } else { + this.timeSeriesChartWidgetConfigForm.get('dataZoomUpdateTimewindow').disable(); + } + if (comparisonEnabled) { this.timeSeriesChartWidgetConfigForm.get('timeForComparison').enable(); this.timeSeriesChartWidgetConfigForm.get('comparisonCustomIntervalValue').enable(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index 0287b775c3..36ba031c8f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -114,6 +114,7 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft ngAfterViewInit() { const settings = barChartWithLabelsTimeSeriesSettings(this.settings); this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, settings, this.chartShape.nativeElement, this.renderer); + this.ctx.widgetActions = this.timeSeriesChart.getWidgetActions(); } ngOnDestroy() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts index b8b879bf70..b045396f2c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts @@ -53,6 +53,7 @@ import { export interface BarChartWithLabelsWidgetSettings extends TimeSeriesChartTooltipWidgetSettings { dataZoom: boolean; + dataZoomUpdateTimewindow: boolean; showBarLabel: boolean; barLabelFont: Font; barLabelColor: string; @@ -79,6 +80,7 @@ export interface BarChartWithLabelsWidgetSettings extends TimeSeriesChartTooltip export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings = { dataZoom: false, + dataZoomUpdateTimewindow: false, showBarLabel: true, barLabelFont: { family: 'Roboto', @@ -181,6 +183,7 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings export const barChartWithLabelsTimeSeriesSettings = (settings: BarChartWithLabelsWidgetSettings): DeepPartial => ({ dataZoom: settings.dataZoom, + dataZoomUpdateTimewindow: settings.dataZoomUpdateTimewindow, grid: settings.grid, yAxes: { default: settings.yAxis diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts index 04f3b5d49b..ad57250036 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts @@ -262,3 +262,9 @@ export const getFocusedSeriesIndex = (chart: ECharts): number => { } return -1; }; + +export interface DataZoomEvent { + start: number; + end: number; + batch?: Array<{ start: number; end: number }>; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts index fe8c51bcee..91ce230539 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts @@ -293,7 +293,9 @@ private static mapTooltipParams(params: CallbackDataParams[] | CallbackDataParam TimeSeriesChartTooltip.appendTooltipItem(result, seriesParams, series); } else if (Array.isArray(params)) { for (seriesParams of params) { - TimeSeriesChartTooltip.appendTooltipItem(result, seriesParams, series); + if (seriesParams.value) { + TimeSeriesChartTooltip.appendTooltipItem(result, seriesParams, series); + } } } return result; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts index aa94ee717c..dc9ddf7cb8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts @@ -137,6 +137,7 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV ngAfterViewInit() { this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, this.settings, this.chartShape.nativeElement, this.renderer); + this.ctx.widgetActions = this.timeSeriesChart.getWidgetActions(); } ngOnDestroy() { 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 e815eefe11..902e2189e2 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 @@ -699,6 +699,7 @@ export interface TimeSeriesChartSettings extends TimeSeriesChartTooltipWidgetSet thresholds: TimeSeriesChartThreshold[]; darkMode: boolean; dataZoom: boolean; + dataZoomUpdateTimewindow: boolean; stack: boolean; grid: TimeSeriesChartGridSettings; yAxes: TimeSeriesChartYAxes; @@ -714,6 +715,7 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { thresholds: [], darkMode: false, dataZoom: true, + dataZoomUpdateTimewindow: false, stack: false, grid: mergeDeep({} as TimeSeriesChartGridSettings, timeSeriesChartGridDefaultSettings), 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 d1928d75f0..7f9f144b87 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 @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { WidgetContext } from '@home/models/widget-component.models'; +import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; import { adjustTimeAxisExtentToData, calculateThresholdsOffset, @@ -23,7 +23,8 @@ import { createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings, generateChartData, - LineSeriesStepType, normalizeAxisLimit, + LineSeriesStepType, + normalizeAxisLimit, parseThresholdData, TimeSeriesChartAxis, TimeSeriesChartDataItem, @@ -47,9 +48,11 @@ import { } from '@home/components/widget/lib/chart/time-series-chart.models'; import { calculateAxisSize, + DataZoomEvent, ECharts, echartsModule, EChartsOption, + getAxis, getAxisExtent, getFocusedSeriesIndex, measureAxisNameSize @@ -141,6 +144,13 @@ export class TbTimeSeriesChart { private hasVisualMap = false; private visualMapSelectedRanges: {[key: number]: boolean}; + private dataZoomResetting = false; + private dataZoomUpdatePending = false; + private dataZoomDebounce: ReturnType | null = null; + private lastDataZoomStart = 0; + private lastDataZoomEnd = 100; + private timewindowChanged = false; + private timeSeriesChart: ECharts; private timeSeriesChartOptions: EChartsOption; @@ -265,6 +275,12 @@ export class TbTimeSeriesChart { if (this.highlightedDataKey) { this.keyEnter(this.highlightedDataKey); } + if (this.dataZoomUpdatePending) { + this.dataZoomUpdatePending = false; + this.dataZoomResetting = true; + this.timeSeriesChart.dispatchAction({ type: 'dataZoom', start: 0, end: 100 }); + this.dataZoomResetting = false; + } } } @@ -386,6 +402,10 @@ export class TbTimeSeriesChart { } public destroy(): void { + if (this.dataZoomDebounce !== null) { + clearTimeout(this.dataZoomDebounce); + this.dataZoomDebounce = null; + } if (this.shapeResize$) { this.shapeResize$.disconnect(); } @@ -417,6 +437,25 @@ export class TbTimeSeriesChart { return this.darkMode; } + public getWidgetActions(): WidgetAction[] { + if (this.settings.dataZoom && this.settings.dataZoomUpdateTimewindow) { + return [{ + name: 'widgets.time-series-chart.reset-timewindow', + icon: 'restore', + onAction: () => { + if (this.dataZoomDebounce !== null) { + clearTimeout(this.dataZoomDebounce); + this.dataZoomDebounce = null; + } + this.timewindowChanged = false; + this.ctx.defaultSubscription.onResetTimewindow(); + }, + show: ()=> this.timewindowChanged + }]; + } + return []; + } + private setupData(): void { const noAggregationBarWidthSettings = this.settings.noAggregationBarWidthSettings; const targetBarWidth = noAggregationBarWidthSettings.strategy === TimeSeriesChartNoAggregationBarWidthStrategy.group ? @@ -846,8 +885,39 @@ export class TbTimeSeriesChart { this.updateAxes(false); if (this.settings.dataZoom) { - this.timeSeriesChart.on('datazoom', () => { + this.timeSeriesChart.on('datazoom', (event: DataZoomEvent) => { this.updateAxes(); + if (this.settings.dataZoomUpdateTimewindow && !this.dataZoomResetting) { + const evStart = event.batch?.length ? event.batch[0].start : event.start; + const evEnd = event.batch?.length ? event.batch[0].end : event.end; + this.lastDataZoomStart = evStart; + this.lastDataZoomEnd = evEnd; + if (this.dataZoomDebounce !== null) { + clearTimeout(this.dataZoomDebounce); + } + this.dataZoomDebounce = setTimeout(() => { + this.dataZoomDebounce = null; + if (this.timeSeriesChart?.isDisposed()) { + return; + } + if (Math.round(this.lastDataZoomStart) === 0 && Math.round(this.lastDataZoomEnd) === 100) { + this.ctx.defaultSubscription.onResetTimewindow(); + this.timewindowChanged = false; + } else { + const axis = getAxis(this.timeSeriesChart, 'xAxis', 'main'); + if (axis) { + const extent = axis.scale.getExtent(); + const startMs = Math.round(extent[0]); + const endMs = Math.round(extent[1]); + if (startMs < endMs) { + this.dataZoomUpdatePending = true; + this.timewindowChanged = true; + this.ctx.defaultSubscription.onUpdateTimewindow(startMs, endMs); + } + } + } + }, 500); + } }); } } 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 bda194828e..793921b4dc 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 @@ -23,6 +23,11 @@ {{ 'widgets.time-series-chart.data-zoom' | translate }}
+
+ + {{ 'widgets.time-series-chart.data-zoom-update-timewindow' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts index 55ed3d5035..9cb686cf78 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts @@ -78,6 +78,7 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom this.barChartWidgetSettingsForm = this.fb.group({ dataZoom: [settings.dataZoom, []], + dataZoomUpdateTimewindow: [settings.dataZoomUpdateTimewindow, []], showBarLabel: [settings.showBarLabel, []], barLabelFont: [settings.barLabelFont, []], @@ -130,10 +131,11 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom } protected validatorTriggers(): string[] { - return ['showBarLabel', 'showBarValue', 'showBarBorder', 'showLegend', 'showTooltip', 'tooltipShowDate']; + return ['dataZoom', 'showBarLabel', 'showBarValue', 'showBarBorder', 'showLegend', 'showTooltip', 'tooltipShowDate']; } protected updateValidators(emitEvent: boolean) { + const dataZoom: boolean = this.barChartWidgetSettingsForm.get('dataZoom').value; const showBarLabel: boolean = this.barChartWidgetSettingsForm.get('showBarLabel').value; const showBarValue: boolean = this.barChartWidgetSettingsForm.get('showBarValue').value; const showBarBorder: boolean = this.barChartWidgetSettingsForm.get('showBarBorder').value; @@ -141,6 +143,12 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom const showTooltip: boolean = this.barChartWidgetSettingsForm.get('showTooltip').value; const tooltipShowDate: boolean = this.barChartWidgetSettingsForm.get('tooltipShowDate').value; + if (dataZoom) { + this.barChartWidgetSettingsForm.get('dataZoomUpdateTimewindow').enable(); + } else { + this.barChartWidgetSettingsForm.get('dataZoomUpdateTimewindow').disable(); + } + if (showBarLabel) { this.barChartWidgetSettingsForm.get('barLabelFont').enable(); this.barChartWidgetSettingsForm.get('barLabelColor').enable(); 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 b49d8ad902..c51bc07eb1 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 @@ -86,6 +86,11 @@ {{ 'widgets.time-series-chart.data-zoom' | translate }}
+
+ + {{ 'widgets.time-series-chart.data-zoom-update-timewindow' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index 8a96f31445..f20320a773 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -135,6 +135,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon thresholds: [settings.thresholds, []], dataZoom: [settings.dataZoom, []], + dataZoomUpdateTimewindow: [settings.dataZoomUpdateTimewindow, []], stack: [settings.stack, []], grid: [settings.grid, []], @@ -181,16 +182,23 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon } protected validatorTriggers(): string[] { - return ['comparisonEnabled', 'showLegend', 'showTooltip', 'tooltipShowDate', 'stack']; + return ['comparisonEnabled', 'dataZoom', 'showLegend', 'showTooltip', 'tooltipShowDate', 'stack']; } protected updateValidators(emitEvent: boolean) { const comparisonEnabled: boolean = this.timeSeriesChartWidgetSettingsForm.get('comparisonEnabled').value; + const dataZoom: boolean = this.timeSeriesChartWidgetSettingsForm.get('dataZoom').value; const showLegend: boolean = this.timeSeriesChartWidgetSettingsForm.get('showLegend').value; const showTooltip: boolean = this.timeSeriesChartWidgetSettingsForm.get('showTooltip').value; const tooltipShowDate: boolean = this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').value; const stack: boolean = this.timeSeriesChartWidgetSettingsForm.get('stack').value; + if (dataZoom) { + this.timeSeriesChartWidgetSettingsForm.get('dataZoomUpdateTimewindow').enable(); + } else { + this.timeSeriesChartWidgetSettingsForm.get('dataZoomUpdateTimewindow').disable(); + } + if (comparisonEnabled) { this.timeSeriesChartWidgetSettingsForm.get('timeForComparison').enable(); this.timeSeriesChartWidgetSettingsForm.get('comparisonCustomIntervalValue').enable(); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index 69cf1de06a..be2ef9d8cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -103,7 +103,7 @@ @for (action of widget.widgetActions; track action) {