diff --git a/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json index e65e9a1204..40399f034b 100644 --- a/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json +++ b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", - "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", + "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-rpc-terminal-widget-settings", @@ -43,4 +43,4 @@ "public": true } ] -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 1585c9b2a9..1ceea2c621 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -101,6 +101,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { } catch (Exception e) { } } + if (value instanceof Long longValue) { + if (longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE) { + value = longValue.intValue(); + } + } return new TbelCfSingleValueArg(ts, value); } 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 7b33c88062..5d7f9ec7d6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -918,7 +918,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List queueStatsList = new ArrayList<>(); for (int i = 0; i < 97; i++) { QueueStats queueStats = new QueueStats(); - queueStats.setQueueName(StringUtils.randomAlphabetic(5)); + queueStats.setQueueName("test" + StringUtils.randomAlphabetic(5)); queueStats.setServiceId(StringUtils.randomAlphabetic(5)); queueStats.setTenantId(savedTenant.getTenantId()); queueStatsList.add(queueStatsService.save(savedTenant.getId(), queueStats)); @@ -934,8 +934,11 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); List entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "queueName"), new EntityKey(EntityKeyType.ENTITY_FIELD, "serviceId")); + List keyFilters = Collections.singletonList( + getEntityFieldStartsWithFilter("queueName", "test") + ); - EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, null); + EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, keyFilters); PageData data = findByQueryAndCheck(query, 97); Assert.assertEquals(10, data.getTotalPages()); @@ -977,10 +980,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest { KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5); KeyFilter activeAlarmTimeToLongFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 30); - KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); - KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); - KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); - KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); + KeyFilter tenantOwnerNameFilter = getEntityFieldEqualFilter("ownerName", TEST_TENANT_NAME); + KeyFilter wrongOwnerNameFilter = getEntityFieldEqualFilter("ownerName", "wrongName"); + KeyFilter tenantOwnerTypeFilter = getEntityFieldEqualFilter("ownerType", "TENANT"); + KeyFilter customerOwnerTypeFilter = getEntityFieldEqualFilter("ownerType", "CUSTOMER"); // all devices with ownerName = TEST TENANT EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); @@ -1026,10 +1029,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest { filter.setDeviceNameFilter(""); KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5); - KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); - KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); - KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); - KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); + KeyFilter tenantOwnerNameFilter = getEntityFieldEqualFilter("ownerName", TEST_TENANT_NAME); + KeyFilter wrongOwnerNameFilter = getEntityFieldEqualFilter("ownerName", "wrongName"); + KeyFilter tenantOwnerTypeFilter = getEntityFieldEqualFilter("ownerType", "TENANT"); + KeyFilter customerOwnerTypeFilter = getEntityFieldEqualFilter("ownerType", "CUSTOMER"); EntityDataSortOrder sortOrder = new EntityDataSortOrder( new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC @@ -1171,15 +1174,23 @@ public class EntityQueryControllerTest extends AbstractControllerTest { return result; } - private KeyFilter getEntityFieldStringEqualToKeyFilter(String keyName, String value) { - KeyFilter tenantOwnerNameFilter = new KeyFilter(); - tenantOwnerNameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName)); - tenantOwnerNameFilter.setValueType(EntityKeyValueType.STRING); - StringFilterPredicate ownerNamePredicate = new StringFilterPredicate(); - ownerNamePredicate.setValue(FilterPredicateValue.fromString(value)); - ownerNamePredicate.setOperation(StringFilterPredicate.StringOperation.EQUAL); - tenantOwnerNameFilter.setPredicate(ownerNamePredicate); - return tenantOwnerNameFilter; + private KeyFilter getEntityFieldEqualFilter(String keyName, String value) { + return getEntityFieldKeyFilter(keyName, value, StringFilterPredicate.StringOperation.EQUAL); + } + + private KeyFilter getEntityFieldStartsWithFilter(String keyName, String value) { + return getEntityFieldKeyFilter(keyName, value, StringFilterPredicate.StringOperation.STARTS_WITH); + } + + private KeyFilter getEntityFieldKeyFilter(String keyName, String value, StringFilterPredicate.StringOperation operation) { + KeyFilter filter = new KeyFilter(); + filter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName)); + filter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromString(value)); + predicate.setOperation(operation); + filter.setPredicate(predicate); + return filter; } private KeyFilter getServerAttributeNumericGreaterThanKeyFilter(String attribute, int value) { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 280bac6bd4..8c714bc0e7 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -20,7 +20,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; @@ -61,7 +61,7 @@ public class ScriptCalculatedFieldStateTest { private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5512071d-5abc-411d-a907-4cdb6539c2eb")); private final AssetId ASSET_ID = new AssetId(UUID.fromString("5bc010ae-bcfd-46c8-98b9-8ee8c8955a76")); - private final SingleValueArgumentEntry assetHumidityArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("assetHumidity", 43.0), 122L); + private final SingleValueArgumentEntry assetHumidityArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("assetHumidity", 86.0), 122L); private final TsRollingArgumentEntry deviceTemperatureArgEntry = createRollingArgEntry(); private final long ts = System.currentTimeMillis(); @@ -72,7 +72,7 @@ public class ScriptCalculatedFieldStateTest { @Autowired private TbelInvokeService tbelInvokeService; - @MockBean + @MockitoBean private ApiLimitService apiLimitService; @BeforeEach @@ -134,6 +134,22 @@ public class ScriptCalculatedFieldStateTest { assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("maxDeviceTemperature", 17.0, "assetHumidity", 43.0))); } + @Test + void testPerformCalculationWithLongEntry() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + "deviceTemperature", deviceTemperatureArgEntry, + "assetHumidity", new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new LongDataEntry("a", 45L), 10L) + )); + + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result).isNotNull(); + Output output = getCalculatedFieldConfig().getOutput(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("maxDeviceTemperature", 17.0, "assetHumidity", 22.5))); + } + @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); @@ -194,7 +210,7 @@ public class ScriptCalculatedFieldStateTest { config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2)); - config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity}"); + config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity / 2 }"); Output output = new Output(); output.setType(OutputType.ATTRIBUTES); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 8c6381ba37..150546d700 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -139,6 +139,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); }); @@ -161,6 +162,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("104.0"); }); @@ -185,6 +187,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { ArrayNode temperatureF = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, "temperatureF"); assertThat(temperatureF).isNotNull(); + assertThat(temperatureF.get(0)).isNotNull(); assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("77.0"); }); @@ -207,6 +210,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("-3.89"); }); @@ -227,6 +231,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0"); }); @@ -245,6 +250,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); }); @@ -267,6 +273,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { // used default value since telemetry is not present JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); @@ -281,6 +288,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); @@ -292,13 +300,14 @@ public class CalculatedFieldTest extends AbstractContainerTest { // login tenant admin testRestClient.getAndSetUserToken(tenantAdminId); - CalculatedField savedCalculatedField = createScriptCalculatedField(deviceProfileId); + CalculatedField savedCalculatedField = createScriptCalculatedField(deviceProfileId, asset.getId()); await().alias("create CF -> perform initial calculation for device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { JsonNode airDensity = testRestClient.getLatestTelemetry(device.getId()); assertThat(airDensity).isNotNull(); + assertThat(airDensity.get("airDensity")).isNotNull(); assertThat(airDensity.get("airDensity").get(0).get("value").asText()).isEqualTo("1.05"); }); @@ -309,6 +318,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode airDensity = testRestClient.getLatestTelemetry(device.getId()); assertThat(airDensity).isNotNull(); + assertThat(airDensity.get("airDensity")).isNotNull(); assertThat(airDensity.get("airDensity").get(0).get("value").asText()).isEqualTo("0.99"); }); @@ -454,10 +464,10 @@ public class CalculatedFieldTest extends AbstractContainerTest { } private CalculatedField createScriptCalculatedField() { - return createScriptCalculatedField(device.getId()); + return createScriptCalculatedField(device.getId(), asset.getId()); } - private CalculatedField createScriptCalculatedField(EntityId entityId) { + private CalculatedField createScriptCalculatedField(EntityId entityId, EntityId refEntityId) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SCRIPT); @@ -467,12 +477,12 @@ public class CalculatedFieldTest extends AbstractContainerTest { ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration(); Argument argument1 = new Argument(); - argument1.setRefEntityId(asset.getId()); + argument1.setRefEntityId(refEntityId); ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, SERVER_SCOPE); argument1.setRefEntityKey(refEntityKey1); Argument argument2 = new Argument(); ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("temperatureInF", ArgumentType.TS_ROLLING, null); - argument2.setTimeWindow(30000L); + argument2.setTimeWindow(300000L); argument2.setLimit(5); argument2.setRefEntityKey(refEntityKey2); diff --git a/msa/black-box-tests/src/test/resources/connectivity.xml b/msa/black-box-tests/src/test/resources/connectivity.xml index 425fbd67eb..7253d87e63 100644 --- a/msa/black-box-tests/src/test/resources/connectivity.xml +++ b/msa/black-box-tests/src/test/resources/connectivity.xml @@ -21,8 +21,10 @@ - - + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9357a8e0ad..fc3da6c968 100755 --- a/pom.xml +++ b/pom.xml @@ -146,6 +146,7 @@ 9.2.0 1.1.10.5 9.10.0 + 4.1.124.Final @@ -895,6 +896,13 @@ + + io.netty + netty-bom + ${netty.version} + pom + import + org.springframework.boot spring-boot-dependencies @@ -909,7 +917,6 @@ pom import - org.thingsboard netty-mqtt diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 67e11430bd..0e559efd46 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -127,6 +127,7 @@ import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.IntervalType; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; @@ -2476,7 +2477,12 @@ public class RestClient implements Closeable { return getTimeseries(entityId, keys, interval, agg, sortOrder != null ? sortOrder.getDirection() : null, pageLink.getStartTime(), pageLink.getEndTime(), 100, useStrictDataTypes); } + @Deprecated public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, SortOrder.Direction sortOrder, Long startTime, Long endTime, Integer limit, boolean useStrictDataTypes) { + return getTimeseries(entityId, keys, interval, null, null, agg, sortOrder, startTime, endTime, limit, useStrictDataTypes); + } + + public List getTimeseries(EntityId entityId, List keys, Long interval, IntervalType intervalType, String timeZone, Aggregation agg, SortOrder.Direction sortOrder, Long startTime, Long endTime, Integer limit, boolean useStrictDataTypes) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); @@ -2490,6 +2496,16 @@ public class RestClient implements Closeable { StringBuilder urlBuilder = new StringBuilder(baseURL); urlBuilder.append("/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&limit={limit}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&orderBy={orderBy}"); + if (intervalType != null) { + urlBuilder.append("&intervalType={intervalType}"); + params.put("intervalType", intervalType.name()); + } + + if (timeZone != null) { + urlBuilder.append("&timeZone={timeZone}"); + params.put("timeZone", timeZone); + } + if (startTime != null) { urlBuilder.append("&startTs={startTs}"); params.put("startTs", String.valueOf(startTime)); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 10bb24199e..48392aa76e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -34,12 +34,11 @@ public class TbAiNodeConfiguration implements NodeConfiguration invalidSystemPrompts() { - String tooLongString = "a".repeat(10_001); + String tooLongString = "a".repeat(500_001); return Stream.of( Arguments.of(""), - Arguments.of(" "), Arguments.of(tooLongString) ); } @@ -213,12 +212,17 @@ class TbAiNodeTest { } static Stream validSystemPrompts() { - String longString = "a".repeat(10_000); + String longString = "a".repeat(500_000); return Stream.of( Arguments.of((String) null), Arguments.of("a"), Arguments.of("Test system prompt"), - Arguments.of(longString) + Arguments.of(longString), + Arguments.of(""" + first sentence + + second sentence + """) ); } @@ -239,7 +243,7 @@ class TbAiNodeTest { } static Stream invalidUserPrompts() { - String tooLongString = "a".repeat(10_001); + String tooLongString = "a".repeat(500_001); return Stream.of( Arguments.of((String) null), Arguments.of(""), @@ -260,11 +264,16 @@ class TbAiNodeTest { } static Stream validUserPrompts() { - String longString = "a".repeat(10_000); + String longString = "a".repeat(500_000); return Stream.of( Arguments.of("a"), Arguments.of("Test user prompt"), - Arguments.of(longString) + Arguments.of(longString), + Arguments.of(""" + first sentence + + second sentence + """) ); } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 1511840c57..9c7c5d28d6 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -38,6 +38,7 @@ import { import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils'; import { Datasource, + datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, DatasourceType, defaultLegendConfig, @@ -49,7 +50,8 @@ import { WidgetConfigMode, WidgetSize, widgetType, - WidgetTypeDescriptor + WidgetTypeDescriptor, + widgetTypeHasTimewindow } from '@app/shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; @@ -234,6 +236,7 @@ export class DashboardUtilsService { public validateAndUpdateWidget(widget: Widget): Widget { widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type); widget = this.validateAndUpdateWidgetTypeFqn(widget); + this.removeTimewindowConfigIfUnused(widget); if (isDefined((widget as any).title)) { delete (widget as any).title; } @@ -294,8 +297,11 @@ export class DashboardUtilsService { } widgetConfig.datasources = this.validateAndUpdateDatasources(widgetConfig.datasources); if (type === widgetType.latest) { - const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); - widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService); + if (datasourcesHasAggregation(widgetConfig.datasources)) { + const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); + widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, + onlyHistoryTimewindow, this.timeService, false); + } } else if (type === widgetType.rpc) { if (widgetConfig.targetDeviceAliasIds && widgetConfig.targetDeviceAliasIds.length) { widgetConfig.targetDevice = { @@ -348,6 +354,33 @@ export class DashboardUtilsService { return widgetConfig; } + private removeTimewindowConfigIfUnused(widget: Widget) { + const widgetHasTimewindow = this.widgetHasTimewindow(widget); + if (!widgetHasTimewindow || widget.config.useDashboardTimewindow) { + delete widget.config.displayTimewindow; + delete widget.config.timewindow; + delete widget.config.timewindowStyle; + + if (!widgetHasTimewindow) { + delete widget.config.useDashboardTimewindow; + } + } + } + + private widgetHasTimewindow(widget: Widget): boolean { + const widgetDefinition = findWidgetModelDefinition(widget); + if (widgetDefinition) { + return widgetDefinition.hasTimewindow(widget); + } + return widgetTypeHasTimewindow(widget.type) + || (widget.type === widgetType.latest && datasourcesHasAggregation(widget.config.datasources)); + } + + public prepareWidgetForSaving(widget: Widget): Widget { + this.removeTimewindowConfigIfUnused(widget); + return widget; + } + public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget { const config = widget.config; config.showTitle = false; diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 353727e006..198737bfed 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -27,7 +27,8 @@ import { serverErrorCodesTranslations } from '@shared/models/constants'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { CompiledTbFunction, - compileTbFunction, GenericFunction, + compileTbFunction, + GenericFunction, isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models'; @@ -196,6 +197,23 @@ export function deleteNullProperties(obj: any) { }); } +export function deleteFalseProperties(obj: Record): void { + if (isUndefinedOrNull(obj)) { + return; + } + Object.keys(obj).forEach((propName) => { + if (obj[propName] === false || isUndefinedOrNull(obj[propName])) { + delete obj[propName]; + } else if (isObject(obj[propName])) { + deleteFalseProperties(obj[propName]); + } else if (Array.isArray(obj[propName])) { + (obj[propName] as any[]).forEach((elem) => { + deleteFalseProperties(elem); + }); + } + }); +} + export function objToBase64(obj: any): string { const json = JSON.stringify(obj); return btoa(encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, @@ -773,6 +791,33 @@ export function deepTrim(obj: T): T { }, (Array.isArray(obj) ? [] : {}) as T); } +export function deepClean | any[]>(obj: T, { + cleanKeys = [] +} = {}): T { + return _.transform(obj, (result, value, key) => { + if (cleanKeys.includes(key)) { + return; + } + if (Array.isArray(value) || isLiteralObject(value)) { + value = deepClean(value, {cleanKeys}); + } + if(isLiteralObject(value) && isEmpty(value)) { + return; + } + if (Array.isArray(value) && !value.length) { + return; + } + if (value === undefined || value === null || value === '' || Number.isNaN(value)) { + return; + } + + if (Array.isArray(result)) { + return result.push(value); + } + result[key] = value; + }); +} + export function generateSecret(length?: number): string { if (isUndefined(length) || length == null) { length = 1; @@ -958,3 +1003,28 @@ export const unwrapModule = (module: any) : any => { return module; } }; + +export const trimDefaultValues = (input: Record, defaults: Record): Record => { + const result: Record = {}; + + for (const key in input) { + if (!(key in defaults)) { + result[key] = input[key]; + } else if (typeof defaults[key] === 'object' && defaults[key] !== null && typeof input[key] === 'object' && input[key] !== null) { + const subPatch = trimDefaultValues(input[key], defaults[key]); + if (Object.keys(subPatch).length > 0) { + result[key] = subPatch; + } + } else if (defaults[key] !== input[key]) { + result[key] = input[key]; + } + } + + for (const key in defaults) { + if (!(key in input)) { + delete result[key]; + } + } + + return result; +} 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 860e615410..5f9c189399 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 @@ -59,7 +59,7 @@ @if (providerFieldsList.includes('personalAccessToken')) { ai-models.personal-access-token - + {{ 'ai-models.personal-access-token-required' | translate }} @@ -115,7 +115,7 @@ @if (providerFieldsList.includes('apiKey')) { ai-models.api-key - + {{ 'ai-models.api-key-required' | translate }} @@ -143,7 +143,7 @@ @if (providerFieldsList.includes('secretAccessKey')) { ai-models.secret-access-key - + {{ 'ai-models.secret-access-key-required' | translate }} 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 c459d66f12..b74c113d9f 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 @@ -41,6 +41,7 @@ import { deepTrim } from '@core/utils'; export interface AIModelDialogData { AIModel?: AiModel; isAdd?: boolean; + name?: string; } @Component({ @@ -111,6 +112,10 @@ export class AIModelDialogComponent 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 a839e98c13..0099fa83a7 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 @@ -1323,6 +1323,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } private addWidgetToDashboard(widget: Widget) { + this.dashboardUtils.prepareWidgetForSaving(widget); if (this.addingLayoutCtx) { this.addWidgetToLayout(widget, this.addingLayoutCtx.id); this.addingLayoutCtx = null; @@ -1410,7 +1411,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC saveWidget() { this.editWidgetComponent.widgetFormGroup.markAsPristine(); - const widget = deepClone(this.editingWidget); + const widget = this.dashboardUtils.prepareWidgetForSaving(deepClone(this.editingWidget)); const widgetLayout = deepClone(this.editingWidgetLayout); const id = this.editingWidgetOriginal.id; this.dashboardConfiguration.widgets[id] = widget; 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 7f7d50db3e..59bfa7ce9f 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 @@ -49,7 +49,7 @@ rule-node-config.password - + {{ 'rule-node-config.password-required' | translate }} @@ -85,7 +85,7 @@ rule-node-config.private-key-password - + 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 f590dae84f..5966448e9f 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 @@ -29,7 +29,7 @@ labelText="rule-node-config.ai.model" (entityChanged)="onEntityChange($event)" [entityType]="entityType.AI_MODEL" - (createNew)="createModelAi('modelId')" + (createNew)="createModelAi($event, 'modelId')" formControlName="modelId"> 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 1ef8ebca72..b6670d7744 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 @@ -51,8 +51,8 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { protected onConfigurationSet(configuration: RuleNodeConfiguration) { this.aiConfigForm = this.fb.group({ modelId: [configuration?.modelId ?? null, [Validators.required]], - systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], - userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], + systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], + userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], responseFormat: this.fb.group({ type: [configuration?.responseFormat?.type ?? ResponseFormat.JSON, []], schema: [configuration?.responseFormat?.schema ?? null, [jsonRequired]], @@ -98,12 +98,13 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { return this.translate.instant(`rule-node-config.ai.response-format-hint-${this.aiConfigForm.get('responseFormat.type').value}`); } - createModelAi(formControl: string) { + createModelAi(name: string, formControl: string) { this.dialog.open(AIModelDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - isAdd: true + isAdd: true, + name } }).afterClosed() .subscribe((model) => { 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 0676af17f9..6adf2e6a4e 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 @@ -64,7 +64,7 @@ rule-node-config.password - + 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 0e9db748bf..545f9ba985 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 @@ -109,7 +109,7 @@ rule-node-config.password - + 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 14d4a227ff..afd272e1cd 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 @@ -25,7 +25,7 @@ admin.aws-secret-access-key - + {{ 'admin.aws-secret-access-key-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts index ec6af2857c..b359a16f51 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts @@ -23,11 +23,19 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, UntypedFormGroup } from '@angular/forms'; -import { DataKey, DatasourceType, Widget, WidgetConfigMode, widgetType } from '@shared/models/widget.models'; +import { + DataKey, + DatasourceType, + Widget, + widgetTypeCanHaveTimewindow, + WidgetConfigMode, + widgetType +} from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { IAliasController } from '@core/api/widget-api.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; @@ -107,6 +115,11 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement if (this.isAdd) { this.setupDefaults(widgetConfig); } + if (widgetTypeCanHaveTimewindow(widgetConfig.widgetType) && isUndefinedOrNull(widgetConfig.config.timewindow)) { + widgetConfig.config.timewindow = initModelFromDefaultTimewindow(null, + widgetConfig.widgetType === widgetType.latest, false, this.widgetConfigComponent.timeService, + widgetConfig.widgetType === widgetType.timeseries); + } this.onConfigSet(widgetConfig); this.updateValidators(false); for (const trigger of this.validatorTriggers()) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html index acd4fc43be..0131eb2947 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html @@ -45,14 +45,14 @@ -