diff --git a/application/src/main/data/json/system/widget_types/html_container.json b/application/src/main/data/json/system/widget_types/html_container.json
index 25b9e5ac5d..a27996593b 100644
--- a/application/src/main/data/json/system/widget_types/html_container.json
+++ b/application/src/main/data/json/system/widget_types/html_container.json
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n",
+ "controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'javaScript': {\n name: 'JavaScript',\n multiple: true\n }\n };\n}",
"settingsDirective": "tb-html-container-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-html-container-basic-config",
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
index 7f21f8d2e7..a837b5ebde 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
@@ -67,6 +67,8 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
ObjectNode valuesNode = JacksonUtil.newObjectNode();
if (result instanceof Double doubleValue) {
valuesNode.put(outputName, doubleValue);
+ } else if (result instanceof Long longValue) {
+ valuesNode.put(outputName, longValue);
} else if (result instanceof Integer integerValue) {
valuesNode.put(outputName, integerValue);
} else {
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java
index 87c1aaa707..4e42b41dd2 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfi
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.queue.TbQueueAdmin;
+import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@@ -45,6 +46,7 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb
private final QueueService queueService;
private final TbClusterService tbClusterService;
private final TbQueueAdmin tbQueueAdmin;
+ private final TopicService topicService;
@Override
public Queue saveQueue(Queue queue) {
@@ -173,9 +175,10 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb
private void createTopicsIfNeeded(Queue queue, Queue oldQueue) {
int newPartitions = queue.getPartitions();
int oldPartitions = oldQueue != null ? oldQueue.getPartitions() : 0;
+ String topic = topicService.buildTopicName(queue.getTopic());
for (int i = oldPartitions; i < newPartitions; i++) {
tbQueueAdmin.createTopicIfNotExists(
- new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(),
+ new TopicPartitionInfo(topic, queue.getTenantId(), i, false).getFullTopicName(),
queue.getCustomProperties(),
true); // forcing topic creation because the topic may still be cached on some nodes
}
diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
index 4454d41e6e..35f637dc09 100644
--- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
@@ -154,7 +154,7 @@ public class SimpleCalculatedFieldStateTest {
Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
- assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49)));
+ assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49L)));
}
@Test
@@ -184,7 +184,7 @@ public class SimpleCalculatedFieldStateTest {
Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
- assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 35)));
+ assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 35L)));
}
@Test
diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueServiceTest.java
new file mode 100644
index 0000000000..81cc325bdb
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueServiceTest.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.service.entitiy.queue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.server.cluster.TbClusterService;
+import org.thingsboard.server.common.data.id.QueueId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.queue.Queue;
+import org.thingsboard.server.dao.queue.QueueService;
+import org.thingsboard.server.queue.TbQueueAdmin;
+import org.thingsboard.server.queue.discovery.TopicService;
+
+import java.util.UUID;
+
+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.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class DefaultTbQueueServiceTest {
+
+ @Mock
+ private QueueService queueServiceMock;
+ @Mock
+ private TbClusterService tbClusterServiceMock;
+ @Mock
+ private TbQueueAdmin tbQueueAdminMock;
+
+ private TopicService topicService;
+ private DefaultTbQueueService tbQueueService;
+
+ private final TenantId tenantId = TenantId.SYS_TENANT_ID;
+
+ @BeforeEach
+ public void setUp() {
+ topicService = new TopicService();
+ tbQueueService = new DefaultTbQueueService(queueServiceMock, tbClusterServiceMock, tbQueueAdminMock, topicService);
+ }
+
+ private Queue newQueue(int partitions) {
+ Queue queue = new Queue();
+ queue.setTenantId(tenantId);
+ queue.setName("testQueue");
+ queue.setTopic("tb_rule_engine.testQueue");
+ queue.setPartitions(partitions);
+ return queue;
+ }
+
+ @Test
+ public void givenQueuePrefix_whenSaveQueue_thenCreatesPrefixedTopics() {
+ // queue.prefix = "thingsboard" (TB_QUEUE_PREFIX set)
+ ReflectionTestUtils.setField(topicService, "prefix", "thingsboard");
+
+ Queue queue = newQueue(2);
+ when(queueServiceMock.saveQueue(queue)).thenReturn(queue);
+
+ tbQueueService.saveQueue(queue);
+
+ ArgumentCaptor topicCaptor = ArgumentCaptor.forClass(String.class);
+ verify(tbQueueAdminMock, times(2)).createTopicIfNotExists(topicCaptor.capture(), any(), anyBoolean());
+
+ // All created topics must carry the prefix - this is the fix.
+ assertThat(topicCaptor.getAllValues())
+ .containsExactlyInAnyOrder(
+ "thingsboard.tb_rule_engine.testQueue.0",
+ "thingsboard.tb_rule_engine.testQueue.1");
+ // No unprefixed (orphan-prone) topic must ever be created.
+ assertThat(topicCaptor.getAllValues())
+ .noneMatch(topic -> topic.equals("tb_rule_engine.testQueue.0")
+ || topic.equals("tb_rule_engine.testQueue.1"));
+ }
+
+ @Test
+ public void givenNoQueuePrefix_whenSaveQueue_thenCreatesUnprefixedTopics() {
+ // queue.prefix blank (TB_QUEUE_PREFIX not set) - default behavior preserved
+ ReflectionTestUtils.setField(topicService, "prefix", "");
+
+ Queue queue = newQueue(2);
+ when(queueServiceMock.saveQueue(queue)).thenReturn(queue);
+
+ tbQueueService.saveQueue(queue);
+
+ ArgumentCaptor topicCaptor = ArgumentCaptor.forClass(String.class);
+ verify(tbQueueAdminMock, times(2)).createTopicIfNotExists(topicCaptor.capture(), any(), anyBoolean());
+
+ assertThat(topicCaptor.getAllValues())
+ .containsExactlyInAnyOrder(
+ "tb_rule_engine.testQueue.0",
+ "tb_rule_engine.testQueue.1");
+ }
+
+ @Test
+ public void givenQueuePrefix_whenIncreasePartitions_thenOnlyNewPartitionsCreatedPrefixed() {
+ ReflectionTestUtils.setField(topicService, "prefix", "thingsboard");
+
+ Queue oldQueue = newQueue(2);
+ oldQueue.setId(new QueueId(UUID.randomUUID()));
+ Queue updatedQueue = newQueue(4);
+ updatedQueue.setId(oldQueue.getId());
+
+ when(queueServiceMock.findQueueById(tenantId, updatedQueue.getId())).thenReturn(oldQueue);
+ when(queueServiceMock.saveQueue(updatedQueue)).thenReturn(updatedQueue);
+
+ tbQueueService.saveQueue(updatedQueue);
+
+ ArgumentCaptor topicCaptor = ArgumentCaptor.forClass(String.class);
+ verify(tbQueueAdminMock, times(2)).createTopicIfNotExists(topicCaptor.capture(), any(), anyBoolean());
+
+ assertThat(topicCaptor.getAllValues())
+ .containsExactlyInAnyOrder(
+ "thingsboard.tb_rule_engine.testQueue.2",
+ "thingsboard.tb_rule_engine.testQueue.3");
+ verify(tbQueueAdminMock, never()).createTopicIfNotExists(eq("thingsboard.tb_rule_engine.testQueue.0"), any(), anyBoolean());
+ }
+
+}
diff --git a/common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java b/common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java
index 6681557c87..293120954d 100644
--- a/common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java
+++ b/common/util/src/main/java/org/thingsboard/common/util/NumberUtils.java
@@ -36,12 +36,16 @@ public class NumberUtils {
return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue();
}
+ public static long toLong(double value) {
+ return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).longValue();
+ }
+
public static Object roundResult(double value, Integer precision) {
if (precision == null) {
return value;
}
if (precision.equals(0)) {
- return toInt(value);
+ return toLong(value);
}
return toFixed(value, precision);
}
diff --git a/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java b/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java
index 1a1bf326e6..dabfe9cffc 100644
--- a/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java
+++ b/common/util/src/test/java/org/thingsboard/common/util/NumberUtilsTest.java
@@ -51,11 +51,20 @@ public class NumberUtilsTest {
assertThat(NumberUtils.toInt(28.0)).isEqualTo(28);
}
+ @Test
+ public void toLong() {
+ assertThat(NumberUtils.toLong(doubleVal)).isEqualTo(1729L);
+ assertThat(NumberUtils.toLong(12.8)).isEqualTo(13L);
+ assertThat(NumberUtils.toLong(28.0)).isEqualTo(28L);
+ assertThat(NumberUtils.toLong(3_980_173_734.0)).isEqualTo(3_980_173_734L);
+ }
+
@Test
public void roundResult() {
assertThat(NumberUtils.roundResult(doubleVal, null)).isEqualTo(1729.1729);
- assertThat(NumberUtils.roundResult(doubleVal, 0)).isEqualTo(1729);
+ assertThat(NumberUtils.roundResult(doubleVal, 0)).isEqualTo(1729L);
assertThat(NumberUtils.roundResult(doubleVal, 2)).isEqualTo(1729.17);
+ assertThat(NumberUtils.roundResult(3_980_173_734.0, 0)).isEqualTo(3_980_173_734L);
}
}
diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts
index 12f5bc60d8..f7c719cc0c 100644
--- a/ui-ngx/src/app/core/api/widget-api.models.ts
+++ b/ui-ngx/src/app/core/api/widget-api.models.ts
@@ -119,6 +119,7 @@ export interface WidgetActionsApi {
elementClick: ($event: Event) => void;
cardClick: ($event: Event) => void;
click: ($event: Event) => void;
+ invokeAction: ($event: Event, actionName: string, additionalParams?: any) => void;
getActiveEntityInfo: () => SubscriptionEntityInfo;
openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => MatDialogRef;
diff --git a/ui-ngx/src/app/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 b493696d4e..043edd53f4 100644
--- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-