From 4bf34281f0810ecf5974b0b8691f3543ec6e638c Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 27 Aug 2024 17:55:41 +0300 Subject: [PATCH 01/29] HttpClient - fixed validation of hostname and port in case proxy usage --- .../org/thingsboard/rule/engine/rest/TbHttpClient.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 96ec648941..8c7dac0b50 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 @@ -336,8 +336,8 @@ public class TbHttpClient { String hostname = properties.getProperty(hostProperty); int port = Integer.parseInt(properties.getProperty(portProperty)); - checkProxyHost(config.getProxyHost()); - checkProxyPort(config.getProxyPort()); + checkProxyHost(hostname); + checkProxyPort(port); var proxy = option .type(ProxyProvider.Proxy.HTTP) @@ -362,8 +362,8 @@ public class TbHttpClient { ProxyProvider.Proxy type = SOCKS_VERSION_5.equals(version) ? ProxyProvider.Proxy.SOCKS5 : ProxyProvider.Proxy.SOCKS4; int port = Integer.parseInt(properties.getProperty(SOCKS_PROXY_PORT)); - checkProxyHost(config.getProxyHost()); - checkProxyPort(config.getProxyPort()); + checkProxyHost(hostname); + checkProxyPort(port); ProxyProvider.Builder proxy = option .type(type) From c38584e79f55ba355f0743adf197067a36ab7857 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 27 Aug 2024 18:04:20 +0300 Subject: [PATCH 02/29] Added new config for TbRestApiCallNodeConfiguration - maxInMemoryBufferSizeInKb --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 1 + .../rule/engine/rest/TbRestApiCallNodeConfiguration.java | 2 ++ 2 files changed, 3 insertions(+) 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 8c7dac0b50..d71cc7f021 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 @@ -132,6 +132,7 @@ public class TbHttpClient { this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset") + .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs().maxInMemorySize(config.getMaxInMemoryBufferSizeInKb() * 1024)) .build(); } catch (SSLException e) { throw new TbNodeException(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 7d2ff7167d..1825bd1ae7 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 @@ -46,6 +46,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration Date: Tue, 27 Aug 2024 19:22:49 +0300 Subject: [PATCH 03/29] TbHttpClient: Add cause to metadata in cause of buffer overloaded exception --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 5 ++++- 1 file changed, 4 insertions(+), 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 d71cc7f021..4830761110 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 @@ -132,7 +132,7 @@ public class TbHttpClient { this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset") - .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs().maxInMemorySize(config.getMaxInMemoryBufferSizeInKb() * 1024)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(config.getMaxInMemoryBufferSizeInKb() * 1024)) .build(); } catch (SSLException e) { throw new TbNodeException(e); @@ -286,6 +286,9 @@ public class TbHttpClient { metaData.putValue(STATUS, restClientResponseException.getStatusText()); metaData.putValue(STATUS_CODE, restClientResponseException.getStatusCode().value() + ""); metaData.putValue(ERROR_BODY, restClientResponseException.getResponseBodyAsString()); + if (restClientResponseException.getStatusCode().is2xxSuccessful()) { + metaData.putValue(ERROR, metaData.getValue(ERROR) + ". Cause: " +restClientResponseException.getCause().getMessage()); + } } return TbMsg.transformMsgMetadata(origMsg, metaData); } From b56f75a99675a110b6efe23fd17162c02e108b98 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 29 Aug 2024 10:45:34 +0300 Subject: [PATCH 04/29] added validation on init --- .../engine/aws/lambda/TbAwsLambdaNode.java | 25 ++++++- .../aws/lambda/TbAwsLambdaNodeTest.java | 72 +++++++++++++++++-- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index 2512125ff1..d20ff2f75c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -62,9 +62,7 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class); - if (StringUtils.isBlank(config.getFunctionName())) { - throw new TbNodeException("Function name must be set!", true); - } + validateConfig(); try { AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()); client = AWSLambdaAsyncClientBuilder.standard() @@ -138,6 +136,27 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { return TbMsg.transformMsgMetadata(origMsg, metaData); } + private void validateConfig() throws TbNodeException { + if (StringUtils.isBlank(config.getFunctionName())) { + throw new TbNodeException("Function name must be set!", true); + } + if (StringUtils.isBlank(config.getAccessKey())) { + throw new TbNodeException("Access Key must be set!", true); + } + if (StringUtils.isBlank(config.getSecretKey())) { + throw new TbNodeException("Secret Access Key must be set!", true); + } + if (StringUtils.isBlank(config.getRegion())) { + throw new TbNodeException("Region must be set!", true); + } + if (config.getConnectionTimeout() < 0) { + throw new TbNodeException("Min connection timeout is 0!", true); + } + if (config.getRequestTimeout() < 0) { + throw new TbNodeException("Min request timeout is 0!", true); + } + } + @Override public void destroy() { if (client != null) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java index 4ee954991c..eed3613a48 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java @@ -75,10 +75,14 @@ public class TbAwsLambdaNodeTest { void setUp() { node = new TbAwsLambdaNode(); config = new TbAwsLambdaNodeConfiguration().defaultConfiguration(); + config.setAccessKey("accessKey"); + config.setSecretKey("secretKey"); + config.setFunctionName("new-function"); } @Test public void verifyDefaultConfig() { + config = new TbAwsLambdaNodeConfiguration().defaultConfiguration(); assertThat(config.getAccessKey()).isNull(); assertThat(config.getSecretKey()).isNull(); assertThat(config.getRegion()).isEqualTo(("us-east-1")); @@ -97,7 +101,70 @@ public class TbAwsLambdaNodeTest { var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); assertThatThrownBy(() -> node.init(ctx, configuration)) .isInstanceOf(TbNodeException.class) - .hasMessage("Function name must be set!"); + .hasMessage("Function name must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) { + config.setAccessKey(accessKey); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Access Key must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) { + config.setSecretKey(secretAccessKey); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Secret Access Key must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + public void givenInvalidRegion_whenInit_thenThrowsException(String region) { + config.setRegion(region); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Region must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @Test + public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() { + config.setConnectionTimeout(-100); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Min connection timeout is 0!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @Test + public void givenInvalidRequestTimeout_whenInit_thenThrowsException() { + config.setRequestTimeout(-100); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Min request timeout is 0!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); } @ParameterizedTest @@ -281,9 +348,6 @@ public class TbAwsLambdaNodeTest { } private void init() { - config.setAccessKey("accessKey"); - config.setSecretKey("secretKey"); - config.setFunctionName("new-function"); ReflectionTestUtils.setField(node, "client", clientMock); ReflectionTestUtils.setField(node, "config", config); } From 5256cae80406a06842cff423a2ecdfe2361fe2b0 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 29 Aug 2024 12:35:30 +0300 Subject: [PATCH 05/29] used jakarta validation constraints --- .../engine/aws/lambda/TbAwsLambdaNode.java | 26 ++------ .../lambda/TbAwsLambdaNodeConfiguration.java | 8 +++ .../aws/lambda/TbAwsLambdaNodeTest.java | 59 +++++++------------ 3 files changed, 32 insertions(+), 61 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index d20ff2f75c..06dc0310ae 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -39,6 +39,8 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; + @Slf4j @RuleNode( type = ComponentType.EXTERNAL, @@ -62,7 +64,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class); - validateConfig(); + String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; + validateFields(config, errorPrefix); try { AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()); client = AWSLambdaAsyncClientBuilder.standard() @@ -136,27 +139,6 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { return TbMsg.transformMsgMetadata(origMsg, metaData); } - private void validateConfig() throws TbNodeException { - if (StringUtils.isBlank(config.getFunctionName())) { - throw new TbNodeException("Function name must be set!", true); - } - if (StringUtils.isBlank(config.getAccessKey())) { - throw new TbNodeException("Access Key must be set!", true); - } - if (StringUtils.isBlank(config.getSecretKey())) { - throw new TbNodeException("Secret Access Key must be set!", true); - } - if (StringUtils.isBlank(config.getRegion())) { - throw new TbNodeException("Region must be set!", true); - } - if (config.getConnectionTimeout() < 0) { - throw new TbNodeException("Min connection timeout is 0!", true); - } - if (config.getRequestTimeout() < 0) { - throw new TbNodeException("Min request timeout is 0!", true); - } - } - @Override public void destroy() { if (client != null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java index c7c66599d5..b395f6b15e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.aws.lambda; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; @@ -23,12 +25,18 @@ public class TbAwsLambdaNodeConfiguration implements NodeConfiguration node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Function name must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -111,12 +107,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) { config.setAccessKey(accessKey); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Access Key must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -124,12 +115,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) { config.setSecretKey(secretAccessKey); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Secret Access Key must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -137,34 +123,19 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidRegion_whenInit_thenThrowsException(String region) { config.setRegion(region); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Region must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @Test public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() { config.setConnectionTimeout(-100); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Min connection timeout is 0!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @Test public void givenInvalidRequestTimeout_whenInit_thenThrowsException() { config.setRequestTimeout(-100); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Min request timeout is 0!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -347,6 +318,16 @@ public class TbAwsLambdaNodeTest { assertThat(throwableCaptor.getValue()).isInstanceOf(AWSLambdaException.class).hasMessageStartingWith(errorMsg); } + private void verifyDataValidationExceptionOnInit() { + RuleNode ruleNode = new RuleNode(); + ruleNode.setName("test"); + when(ctx.getSelf()).thenReturn(ruleNode); + String errorPrefix = "'test' node configuration is invalid: "; + assertThatThrownBy(() -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining(errorPrefix); + } + private void init() { ReflectionTestUtils.setField(node, "client", clientMock); ReflectionTestUtils.setField(node, "config", config); From 12415c15185f2e6a9060411af78308c1aa38b6eb Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 29 Aug 2024 16:11:28 +0300 Subject: [PATCH 06/29] UI: bug fix --- .../json/system/scada_symbols/stand-filter.svg | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/stand-filter.svg b/application/src/main/data/json/system/scada_symbols/stand-filter.svg index 87d578605d..ebd1e28edd 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-filter.svg @@ -7,7 +7,7 @@ ], "widgetSizeX": 3, "widgetSizeY": 5, - "stateRenderFunction": "", + "stateRenderFunction": "var running = ctx.values.running;\nif (running) {\n ctx.api.enable(svg.children());\n} else {\n ctx.api.disable(svg.children());\n}", "tags": [ { "tag": "background", @@ -25,7 +25,7 @@ }, { "tag": "filterMode", - "stateRenderFunction": "var defaultBorderColor = ctx.properties.defaultBorderColor;\nvar activeBorderColor = ctx.properties.activeBorderColor;\nvar defaultLabelColor = ctx.properties.defaultLabelColor;\nvar activeLabelColor = ctx.properties.activeLabelColor;\nvar boxBackground = ctx.properties.modeBoxBackground;\n\nvar running = ctx.values.running;\nvar filtrationMode = ctx.values.filtrationMode;\n\nvar filtrationMap = {};\nvar bottomShift = 104;\nvar rightShift = 226;\nvar middleShift = 121;\n\nvar filterModeSet = element.remember('filterModeSet');\n\nif (!filterModeSet) {\n element.remember('filterModeSet', true);\n var clone = element.children()[0];\n setFilterModeColors(clone);\n element.clear();\n \n var i = 0;\n if (ctx.properties.filtrationMode) {\n i++;\n filtrationMap[i] = 'filter';\n }\n if (ctx.properties.wasteMode) {\n i++;\n filtrationMap[i] = 'waste';\n }\n if (ctx.properties.backwashMode) {\n i++;\n filtrationMap[i] = 'backwash';\n }\n if (ctx.properties.recirculateMode) {\n i++;\n filtrationMap[i] = 'recirculate';\n }\n if (ctx.properties.rinseMode) {\n i++;\n filtrationMap[i] = 'rinse';\n }\n if (ctx.properties.closedMode) {\n i++;\n filtrationMap[i] = 'closed';\n }\n \n var filterMode = Object.values(filtrationMap);\n var lastToMiddle = filterMode.length % 2;\n \n filterMode.forEach((mode, index, arr) => {\n var template = clone.clone();\n var x = (index % 2) * rightShift;\n var y = Math.floor((index % filterMode.length) / 2) * bottomShift;\n if (index === filterMode.length-1 && lastToMiddle) {\n x = middleShift;\n }\n template.attr({'class': mode}).css('cursor', 'pointer').translate(x, y);\n ctx.api.text(template.findOne('text'), capitalizeFirstLetter(mode));\n template.click((event) => click(event, getFilterModeKey(mode)));\n element.add(template);\n })\n if (isFinite(filtrationMode)) {\n setFilterModeColorsByMap(filtrationMode, running);\n }\n}\n\nif (running) {\n ctx.api.enable(element.children());\n} else {\n ctx.api.disable(element.children());\n}\n\nfunction click(event, index) {\n var filtrationMode = ctx.values.filtrationMode;\n if (ctx.values.running && isFinite(filtrationMode)) {\n ctx.api.disable(element.children());\n var newValue = +index;\n if (newValue === filtrationMode) {\n newValue = 0;\n } else {\n setFilterModeColorsByMap(filtrationMode);\n }\n ctx.api.setValue('filtrationMode', newValue);\n ctx.api.callAction(event, 'filtrationModeUpdateState', newValue, {\n next: () => {\n setFilterModeColorsByMap(newValue ? newValue: filtrationMode, newValue);\n ctx.api.enable(element.children());\n },\n error: () => {\n setFilterModeColorsByMap(newValue);\n ctx.api.setValue('filtrationMode', filtrationMode);\n }\n });\n }\n}\n\nfunction getFilterModeKey(value) {\n return Object.keys(filtrationMap).find(key => filtrationMap[key] === value);\n}\n\nfunction setFilterModeColorsByMap(mode, active = false) {\n var filterBox = element.findOne('g.'+filtrationMap[mode])\n if (filterBox) {\n return setFilterModeColors(filterBox, active);\n }\n}\n\nfunction setFilterModeColors(filterBox, active = false) {\n if (filterBox) {\n var borderColor = active ? activeBorderColor : defaultBorderColor;\n var labelColor = active ? activeLabelColor : defaultLabelColor;\n filterBox.children()[0].fill(boxBackground);\n filterBox.children()[1].stroke(borderColor);\n filterBox.children()[2].fill(borderColor);\n ctx.api.font(filterBox.findOne('text'), null, labelColor);\n }\n}\n\nfunction capitalizeFirstLetter(string) {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}", + "stateRenderFunction": "var defaultBorderColor = ctx.properties.defaultBorderColor;\nvar activeBorderColor = ctx.properties.activeBorderColor;\nvar defaultLabelColor = ctx.properties.defaultLabelColor;\nvar activeLabelColor = ctx.properties.activeLabelColor;\nvar boxBackground = ctx.properties.modeBoxBackground;\n\nvar running = ctx.values.running;\nvar filtrationMode = ctx.values.filtrationMode;\n\nvar filtrationMap = {};\nvar bottomShift = 104;\nvar rightShift = 226;\nvar middleShift = 121;\n\nvar filterModeSet = element.remember('filterModeSet');\n\nvar i = 0;\nif (ctx.properties.filtrationMode) {\n i++;\n filtrationMap[i] = 'filter';\n}\nif (ctx.properties.wasteMode) {\n i++;\n filtrationMap[i] = 'waste';\n}\nif (ctx.properties.backwashMode) {\n i++;\n filtrationMap[i] = 'backwash';\n}\nif (ctx.properties.recirculateMode) {\n i++;\n filtrationMap[i] = 'recirculate';\n}\nif (ctx.properties.rinseMode) {\n i++;\n filtrationMap[i] = 'rinse';\n}\nif (ctx.properties.closedMode) {\n i++;\n filtrationMap[i] = 'closed';\n}\n\nif (!filterModeSet) {\n element.remember('filterModeSet', true);\n var clone = element.children()[0];\n setFilterModeColors(clone);\n element.clear();\n \n var filterMode = Object.values(filtrationMap);\n var lastToMiddle = filterMode.length % 2;\n \n filterMode.forEach((mode, index, arr) => {\n var template = clone.clone();\n var x = (index % 2) * rightShift;\n var y = Math.floor((index % filterMode.length) / 2) * bottomShift;\n if (index === filterMode.length-1 && lastToMiddle) {\n x = middleShift;\n }\n template.attr({'class': mode}).css('cursor', 'pointer').translate(x, y);\n ctx.api.text(template.findOne('text'), capitalizeFirstLetter(mode));\n template.click((event) => click(event, getFilterModeKey(mode)));\n element.add(template);\n })\n}\n\nif (isFinite(filtrationMode)) {\n if (element.findOne('.active')) {\n setFilterModeColors(element.findOne('.active'));\n }\n setFilterModeColorsByMap(filtrationMode, running);\n}\n\nfunction click(event, index) {\n var filtrationMode = ctx.values.filtrationMode;\n if (ctx.values.running && isFinite(filtrationMode)) {\n ctx.api.disable(element.children());\n var newValue = +index;\n if (newValue === filtrationMode) {\n newValue = 0;\n } else {\n setFilterModeColorsByMap(filtrationMode);\n }\n ctx.api.setValue('filtrationMode', newValue);\n ctx.api.callAction(event, 'filtrationModeUpdateState', newValue, {\n next: () => {\n setFilterModeColorsByMap(newValue ? newValue: filtrationMode, newValue);\n ctx.api.enable(element.children());\n },\n error: () => {\n setFilterModeColorsByMap(newValue);\n ctx.api.setValue('filtrationMode', filtrationMode);\n ctx.api.enable(element.children());\n }\n });\n }\n}\n\nfunction getFilterModeKey(value) {\n return Object.keys(filtrationMap).find(key => filtrationMap[key] === value);\n}\n\nfunction setFilterModeColorsByMap(mode, active = false) {\n var filterBox = element.findOne('g.'+filtrationMap[mode])\n if (filterBox) {\n return setFilterModeColors(filterBox, active);\n }\n}\n\nfunction setFilterModeColors(filterBox, active = false) {\n if (filterBox) {\n if (active) {\n filterBox.addClass('active');\n } else {\n filterBox.removeClass('active');\n }\n var borderColor = active ? activeBorderColor : defaultBorderColor;\n var labelColor = active ? activeLabelColor : defaultLabelColor;\n filterBox.children()[0].fill(boxBackground);\n filterBox.children()[1].stroke(borderColor);\n filterBox.children()[2].fill(borderColor);\n ctx.api.font(filterBox.findOne('text'), null, labelColor);\n }\n}\n\nfunction capitalizeFirstLetter(string) {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}", "actions": null } ], @@ -159,7 +159,7 @@ "id": "filtrationMode", "name": "{i18n:scada.symbol.filter-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -175,7 +175,7 @@ "id": "wasteMode", "name": "{i18n:scada.symbol.waste-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -191,7 +191,7 @@ "id": "backwashMode", "name": "{i18n:scada.symbol.backwash-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -207,7 +207,7 @@ "id": "recirculateMode", "name": "{i18n:scada.symbol.recirculate-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -223,7 +223,7 @@ "id": "rinseMode", "name": "{i18n:scada.symbol.rinse-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -239,7 +239,7 @@ "id": "closedMode", "name": "{i18n:scada.symbol.closed-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, From 9593f97582e292b78ad6de11c766a88e9936ef6f Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 29 Aug 2024 17:05:53 +0300 Subject: [PATCH 07/29] TbHttpClient: use default 256 KB for maxInMemorySize in case 0 set in configuration --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 3 ++- 1 file changed, 2 insertions(+), 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 4830761110..104e477346 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 @@ -132,7 +132,8 @@ public class TbHttpClient { this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset") - .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(config.getMaxInMemoryBufferSizeInKb() * 1024)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize( + (config.getMaxInMemoryBufferSizeInKb() > 0 ? config.getMaxInMemoryBufferSizeInKb() : 256) * 1024)) .build(); } catch (SSLException e) { throw new TbNodeException(e); From 4c3e1c62f2f77626d755327de555c12bba21ffc2 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 29 Aug 2024 18:24:12 +0300 Subject: [PATCH 08/29] tbel: fix bug ExecutionHashMap.entrySet element.key element.value Update version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 09cf0393b7..aa2a50bc8b 100755 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ 3.9.2 3.25.3 1.63.0 - 1.2.2 + 1.2.3 1.18.32 1.2.5 1.2.5 From f9c2aa3581c123d6c549bafc5372ef60370ef960 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 30 Aug 2024 10:18:40 +0300 Subject: [PATCH 09/29] TbHttpClient: return cause instead of original exception in case 2xx status code --- .../rule/engine/rest/TbHttpClient.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 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 104e477346..839a7633fc 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 @@ -209,13 +209,23 @@ public class TbHttpClient { semaphore.release(); } - onFailure.accept(processException(msg, throwable), throwable); + onFailure.accept(processException(msg, throwable), processThrowable(throwable)); }); } catch (InterruptedException e) { log.warn("Timeout during waiting for reply!", e); } } + private Throwable processThrowable(Throwable origin) { + 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 + return new RuntimeException(restClientResponseException.getCause()); + } + return origin; + } + public URI buildEncodedUri(String endpointUrl) { if (endpointUrl == null) { throw new RuntimeException("Url string cannot be null!"); @@ -287,9 +297,6 @@ public class TbHttpClient { metaData.putValue(STATUS, restClientResponseException.getStatusText()); metaData.putValue(STATUS_CODE, restClientResponseException.getStatusCode().value() + ""); metaData.putValue(ERROR_BODY, restClientResponseException.getResponseBodyAsString()); - if (restClientResponseException.getStatusCode().is2xxSuccessful()) { - metaData.putValue(ERROR, metaData.getValue(ERROR) + ". Cause: " +restClientResponseException.getCause().getMessage()); - } } return TbMsg.transformMsgMetadata(origMsg, metaData); } From e1d11c913a7d202057619a1c07c55bd312a50a67 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 30 Aug 2024 14:33:36 +0300 Subject: [PATCH 10/29] UI: Water meter SCADA symbol --- .../left-analog-water-level-meter.svg | 641 ++++++++++++++++++ .../right-analog-water-level-meter.svg | 641 ++++++++++++++++++ .../scada_water_system_symbols.json | 2 + .../assets/locale/locale.constant-en_US.json | 6 +- 4 files changed, 1289 insertions(+), 1 deletion(-) create mode 100644 application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg create mode 100644 application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg diff --git a/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg new file mode 100644 index 0000000000..b2e47991dc --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg @@ -0,0 +1,641 @@ + +{ + "title": "Left analog water level meter", + "description": "Left analog water level meter", + "searchTags": [ + "water meter" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "", + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.backgroundColor;\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "border", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.defaultBorderColor;\nif (ctx.values.critical) {\n colorProcessor = ctx.properties.criticalBorderColor;\n} else if (ctx.values.warning) {\n colorProcessor = ctx.properties.warningBorderColor;\n} else if (value) {\n colorProcessor = ctx.properties.activeBorderColor;\n}\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "broken", + "stateRenderFunction": "var broken = ctx.values.broken;\nif (broken) {\n element.show();\n} else {\n element.hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "icon", + "stateRenderFunction": "var icon = ctx.properties.icon;\nif (icon) {\n element.show();\n} else {\n element.hide()\n}", + "actions": null + }, + { + "tag": "label", + "stateRenderFunction": "var label = ctx.properties.label;\nif (label) {\n var icon = ctx.properties.icon;\n ctx.api.text(element, ctx.properties.labelText);\n if (!icon) {\n element.transform({translateX: 10});\n }\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pointer", + "stateRenderFunction": "var valueSet = element.remember('valueSet');\nif (!valueSet) {\n element.remember('valueSet', true);\n element.transform({\n rotate: 0\n });\n}\n\nvar value = ctx.values.value;\nvar fullValue = ctx.values.fullValue;\n\nvar degrees = Math.max(0, Math.min(1, value/fullValue))*179.99;\nvar rotate = element.remember('rotate');\nif (degrees !== rotate) {\n element.remember('rotate', degrees);\n ctx.api.animate(element, 1000).ease('-').transform({rotate: degrees});\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "fullValue", + "name": "{i18n:scada.symbol.full-value}", + "hint": "{i18n:scada.symbol.full-value-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "fullValue" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "value", + "name": "{i18n:scada.symbol.value}", + "hint": null, + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "waterLevel" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": "", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": "", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": "", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "broken", + "name": "{i18n:scada.symbol.broken}", + "hint": "{i18n:scada.symbol.broken-hint}", + "group": "", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.broken}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "displayClick", + "name": "{i18n:scada.symbol.on-display-click}", + "hint": "{i18n:scada.symbol.on-display-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "label", + "name": "{i18n:scada.symbol.label}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "labelText", + "name": "{i18n:scada.symbol.label}", + "type": "text", + "default": "Water", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "label", + "rowClass": "", + "fieldClass": "flex", + "min": null, + "max": null, + "step": null + }, + { + "id": "icon", + "name": "{i18n:scada.symbol.icon}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultBorderColor", + "name": "{i18n:scada.symbol.default-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#4A4848", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": "", + "divider": false, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeBorderColor", + "name": "{i18n:scada.symbol.active-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1C943E", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningBorderColor", + "name": "{i18n:scada.symbol.warning-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FAA405", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalBorderColor", + "name": "{i18n:scada.symbol.critical-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#D12730", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FFFFFF", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + Water + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg new file mode 100644 index 0000000000..f8f4ceb4b0 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg @@ -0,0 +1,641 @@ + +{ + "title": "Rigth analog water level meter", + "description": "Right analog water level meter", + "searchTags": [ + "water meter" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "", + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.backgroundColor;\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "border", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.defaultBorderColor;\nif (ctx.values.critical) {\n colorProcessor = ctx.properties.criticalBorderColor;\n} else if (ctx.values.warning) {\n colorProcessor = ctx.properties.warningBorderColor;\n} else if (value) {\n colorProcessor = ctx.properties.activeBorderColor;\n}\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "broken", + "stateRenderFunction": "var broken = ctx.values.broken;\nif (broken) {\n element.show();\n} else {\n element.hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "icon", + "stateRenderFunction": "var icon = ctx.properties.icon;\nif (icon) {\n element.show();\n} else {\n element.hide()\n}", + "actions": null + }, + { + "tag": "label", + "stateRenderFunction": "var label = ctx.properties.label;\nif (label) {\n var icon = ctx.properties.icon;\n ctx.api.text(element, ctx.properties.labelText);\n if (!icon) {\n element.transform({translateX: 10});\n }\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pointer", + "stateRenderFunction": "var valueSet = element.remember('valueSet');\nif (!valueSet) {\n element.remember('valueSet', true);\n element.transform({\n rotate: 0\n });\n}\n\nvar value = ctx.values.value;\nvar fullValue = ctx.values.fullValue;\n\nvar degrees = Math.max(0, Math.min(1, value/fullValue))*179.99;\nvar rotate = element.remember('rotate');\nif (degrees !== rotate) {\n element.remember('rotate', degrees);\n ctx.api.animate(element, 1000).ease('-').transform({rotate: degrees});\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "fullValue", + "name": "{i18n:scada.symbol.full-value}", + "hint": "{i18n:scada.symbol.full-value-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "fullValue" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "value", + "name": "{i18n:scada.symbol.value}", + "hint": null, + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "waterLevel" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": "", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": "", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": "", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "broken", + "name": "{i18n:scada.symbol.broken}", + "hint": "{i18n:scada.symbol.broken-hint}", + "group": "", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.broken}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "displayClick", + "name": "{i18n:scada.symbol.on-display-click}", + "hint": "{i18n:scada.symbol.on-display-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "label", + "name": "{i18n:scada.symbol.label}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "labelText", + "name": "{i18n:scada.symbol.label}", + "type": "text", + "default": "Water", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "label", + "rowClass": "", + "fieldClass": "flex", + "min": null, + "max": null, + "step": null + }, + { + "id": "icon", + "name": "{i18n:scada.symbol.icon}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultBorderColor", + "name": "{i18n:scada.symbol.default-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#4A4848", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": "", + "divider": false, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeBorderColor", + "name": "{i18n:scada.symbol.active-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1C943E", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningBorderColor", + "name": "{i18n:scada.symbol.warning-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FAA405", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalBorderColor", + "name": "{i18n:scada.symbol.critical-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#D12730", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FFFFFF", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + Water + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json index 6f0edcd02f..181a7480ba 100644 --- a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json +++ b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json @@ -34,6 +34,8 @@ "left_flow_meter", "horizontal_inline_flow_meter", "vertical_inline_flow_meter", + "left_analog_water_level_meter", + "right_analog_water_level_meter", "centrifugal_pump", "small_right_motor_pump", "small_left_motor_pump", 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 d9259eec78..df3c05eec4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3875,7 +3875,11 @@ "stand-filter-color": "Stand filter color", "mode-box-background": "Mode box background", "border-color": "Border color", - "label-color": "Label color" + "label-color": "Label color", + "full-value": "Full value", + "full-value-hint": "Double value indicating full value.", + "label": "Label", + "icon": "Icon" } }, "item": { From 5f112b211205a1624c5a8f6bd938524a4e4d9899 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 2 Sep 2024 16:53:45 +0300 Subject: [PATCH 11/29] UI: Add leak sensor and water stop symbol --- .../json/system/scada_symbols/leak-sensor.svg | 158 ++++++++++ .../json/system/scada_symbols/waterstop.svg | 276 ++++++++++++++++++ .../scada_water_system_symbols.json | 2 + .../assets/locale/locale.constant-en_US.json | 5 +- 4 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 application/src/main/data/json/system/scada_symbols/leak-sensor.svg create mode 100644 application/src/main/data/json/system/scada_symbols/waterstop.svg diff --git a/application/src/main/data/json/system/scada_symbols/leak-sensor.svg b/application/src/main/data/json/system/scada_symbols/leak-sensor.svg new file mode 100644 index 0000000000..506385d7fb --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/leak-sensor.svg @@ -0,0 +1,158 @@ + +{ + "title": "Leak sensor", + "description": "Leak sensor", + "searchTags": [ + "leak" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var fill = ctx.properties.backgroundColor;\nelement.attr({fill: fill});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "icon", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.attr({stroke: ctx.properties.leakColor});\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n} else {\n element.attr({stroke: ctx.properties.defaultColor});\n ctx.api.resetAnimation(element);\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.water-leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "leak" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultColor", + "name": "{i18n:scada.symbol.default-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "leakColor", + "name": "{i18n:scada.symbol.leak-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/waterstop.svg b/application/src/main/data/json/system/scada_symbols/waterstop.svg new file mode 100644 index 0000000000..e1ed0e0fd5 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/waterstop.svg @@ -0,0 +1,276 @@ + +{ + "title": "Water stop", + "description": "Remotely controlled water shutoff valve with configurable connectors and various states.", + "searchTags": [ + "water stop" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "", + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "var opened = ctx.values.opened;\nvar action = opened ? 'close' : 'open';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('opened', !opened);\n }\n});" + } + } + }, + { + "tag": "pipe-color", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "walve", + "stateRenderFunction": "var opened = ctx.values.opened;\nif (opened) {\n element.attr({fill: ctx.properties.openedColor});\n ctx.api.resetAnimation(element);\n} else {\n element.attr({fill: ctx.properties.closedColor});\n if (ctx.values.criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "opened", + "name": "{i18n:scada.symbol.opened}", + "hint": "{i18n:scada.symbol.opened-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.opened}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "open", + "name": "{i18n:scada.symbol.open}", + "hint": "{i18n:scada.symbol.open-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "close", + "name": "{i18n:scada.symbol.close}", + "hint": "{i18n:scada.symbol.close-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": true, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "openedColor", + "name": "{i18n:scada.symbol.opened-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "closedColor", + "name": "{i18n:scada.symbol.closed-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json index 6f0edcd02f..619e336daf 100644 --- a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json +++ b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json @@ -34,6 +34,7 @@ "left_flow_meter", "horizontal_inline_flow_meter", "vertical_inline_flow_meter", + "leak_sensor", "centrifugal_pump", "small_right_motor_pump", "small_left_motor_pump", @@ -50,6 +51,7 @@ "vertical_wheel_valve", "horizontal_ball_valve", "vertical_ball_valve", + "water_stop", "vertical_tank", "stand_vertical_tank", "cylindrical_tank", 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 d9259eec78..cc28a4edd3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3875,7 +3875,10 @@ "stand-filter-color": "Stand filter color", "mode-box-background": "Mode box background", "border-color": "Border color", - "label-color": "Label color" + "label-color": "Label color", + "water-leak-hint": "Indicates whether a leak.", + "default-color": "Default color", + "leak-color": "Leak color" } }, "item": { From b86a9d3802ec6a4bb25fcb5f370b99079d33383e Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 2 Sep 2024 21:44:23 +0300 Subject: [PATCH 12/29] tbel: refactoring tbUtils with docs --- .../thingsboard/script/api/tbel/TbUtils.java | 28 +++--- .../script/api/tbel/TbUtilsTest.java | 85 ++++++++++++------- 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index d188111cc3..6651f26cc5 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -273,13 +273,13 @@ public class TbUtils { Long.class, boolean.class, boolean.class))); parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex", Long.class, boolean.class, boolean.class, int.class))); - parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString", Long.class))); - parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString", Long.class, int.class))); - parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString", Long.class, int.class, boolean.class))); - parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString", Long.class, int.class, boolean.class, boolean.class))); parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex", Float.class))); @@ -327,7 +327,7 @@ public class TbUtils { String.class))); parserConfig.addImport("isHexadecimal", new MethodStub(TbUtils.class.getMethod("isHexadecimal", String.class))); - parserConfig.addImport("byteArrayToExecutionArrayList", new MethodStub(TbUtils.class.getMethod("bytesToExecutionArrayList", + parserConfig.addImport("bytesToExecutionArrayList", new MethodStub(TbUtils.class.getMethod("bytesToExecutionArrayList", ExecutionContext.class, byte[].class))); parserConfig.addImport("padStart", new MethodStub(TbUtils.class.getMethod("padStart", String.class, int.class, char.class))); @@ -725,19 +725,19 @@ public class TbUtils { return prepareNumberHexString(l, bigEndian, pref, len, HEX_LEN_LONG_MAX); } - public static String intLongToString(Long number) { - return intLongToString(number, DEC_RADIX); + public static String intLongToRadixString(Long number) { + return intLongToRadixString(number, DEC_RADIX); } - public static String intLongToString(Long number, int radix) { - return intLongToString(number, radix, true); + public static String intLongToRadixString(Long number, int radix) { + return intLongToRadixString(number, radix, true); } - public static String intLongToString(Long number, int radix, boolean bigEndian) { - return intLongToString(number, radix, bigEndian, false); + public static String intLongToRadixString(Long number, int radix, boolean bigEndian) { + return intLongToRadixString(number, radix, bigEndian, false); } - public static String intLongToString(Long number, int radix, boolean bigEndian, boolean pref) { + public static String intLongToRadixString(Long number, int radix, boolean bigEndian, boolean pref) { if (radix >= 25 && radix <= MAX_RADIX) { return Long.toString(number, radix); } @@ -1344,8 +1344,8 @@ public class TbUtils { * Writes the bit value to the appropriate location in the bins array, starting at the end of the array, * to ensure proper alignment (highest bit to low end). */ - public static byte[] parseLongToBinaryArray(long longValue, int binsLength) { - int len = Math.min(binsLength, BYTES_LEN_LONG_MAX * BIN_LEN_MAX); + public static byte[] parseLongToBinaryArray(long longValue, int binLength) { + int len = Math.min(binLength, BYTES_LEN_LONG_MAX * BIN_LEN_MAX); byte[] bins = new byte[len]; for (int i = 0; i < len; i++) { bins[len - 1 - i] = (byte) ((longValue >> i) & 1); diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 0c22f62f1f..2e36b3df42 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -330,8 +330,8 @@ public class TbUtilsTest { @Test public void parseBytesIntToFloat() { byte[] intValByte = {0x00, 0x00, 0x00, 0x0A}; - Float valueExpected = 10.0f; - Float valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, true); + float valueExpected = 10.0f; + float valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, true); Assertions.assertEquals(valueExpected, valueActual); valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, false); Assertions.assertEquals(valueExpected, valueActual); @@ -345,20 +345,23 @@ public class TbUtilsTest { valueExpected = 10.0f; valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, true); Assertions.assertEquals(valueExpected, valueActual); - valueExpected = 1.6777216E8f; - valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, false); + valueExpected = 167772.16f; + double factor = 1e3; + valueActual = (float) (TbUtils.parseBytesIntToFloat(intValByte, 0, 4, false) / factor); + Assertions.assertEquals(valueExpected, valueActual); String dataAT101 = "0x01756403671B01048836BF7701F000090722050000"; List byteAT101 = TbUtils.hexToBytes(ctx, dataAT101); float latitudeExpected = 24.62495f; int offset = 9; - valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset, 4, false); - Assertions.assertEquals(latitudeExpected, valueActual / 1000000); + factor = 1e6; + valueActual = (float) (TbUtils.parseBytesIntToFloat(byteAT101, offset, 4, false) / factor); + Assertions.assertEquals(latitudeExpected, valueActual); float longitudeExpected = 118.030576f; - valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset + 4, 4, false); - Assertions.assertEquals(longitudeExpected, valueActual / 1000000); + valueActual = (float) (TbUtils.parseBytesIntToFloat(byteAT101, offset + 4, 4, false) / factor); + Assertions.assertEquals(longitudeExpected, valueActual); valueExpected = 9.185175E8f; valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset); @@ -520,6 +523,18 @@ public class TbUtilsTest { valueExpected = 7.2057594037927936E17d; valueActual = TbUtils.parseBytesLongToDouble(longValByte, 0, 8, false); Assertions.assertEquals(valueExpected, valueActual); + + String dataPalacKiyv = "0x32D009423F23B300B0106E08D96B6C00"; + List byteAT101 = TbUtils.hexToBytes(ctx, dataPalacKiyv); + double latitudeExpected = 50.422775429058610d; + int offset = 0; + double factor = 1e15; + valueActual = TbUtils.parseBytesLongToDouble(byteAT101, offset, 8, false); + Assertions.assertEquals(latitudeExpected, valueActual / factor); + + double longitudeExpected = 30.517877378257072d; + valueActual = TbUtils.parseBytesLongToDouble(byteAT101, offset + 8, 8, false); + Assertions.assertEquals(longitudeExpected, valueActual / factor); } @Test @@ -720,27 +735,27 @@ public class TbUtilsTest { @Test public void numberToString_Test() { - Assertions.assertEquals("00111010", TbUtils.intLongToString(58L, MIN_RADIX)); - Assertions.assertEquals("0000000010011110", TbUtils.intLongToString(158L, MIN_RADIX)); - Assertions.assertEquals("00000000000000100000001000000001", TbUtils.intLongToString(131585L, MIN_RADIX)); - Assertions.assertEquals("0111111111111111111111111111111111111111111111111111111111111111", TbUtils.intLongToString(Long.MAX_VALUE, MIN_RADIX)); - Assertions.assertEquals("1000000000000000000000000000000000000000000000000000000000000001", TbUtils.intLongToString(-Long.MAX_VALUE, MIN_RADIX)); - Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToString(-102L, MIN_RADIX)); - Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToString(-13158L, MIN_RADIX)); - Assertions.assertEquals("777777777777777777777", TbUtils.intLongToString(Long.MAX_VALUE, 8)); - Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToString(Long.MIN_VALUE, 8)); - Assertions.assertEquals("9223372036854775807", TbUtils.intLongToString(Long.MAX_VALUE)); - Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToString(Long.MIN_VALUE)); - Assertions.assertEquals("3366", TbUtils.intLongToString(13158L, 16)); - Assertions.assertEquals("FFCC9A", TbUtils.intLongToString(-13158L, 16)); - Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToString(-13158L, 16, true, true)); - - Assertions.assertEquals("0x0400", TbUtils.intLongToString(1024L, 16, true, true)); - Assertions.assertNotEquals("400", TbUtils.intLongToString(1024L, 16)); - Assertions.assertEquals("0xFFFC00", TbUtils.intLongToString(-1024L, 16, true, true)); - Assertions.assertNotEquals("0xFC00", TbUtils.intLongToString(-1024L, 16, true, true)); - - Assertions.assertEquals("hazelnut", TbUtils.intLongToString(1356099454469L, MAX_RADIX)); + Assertions.assertEquals("00111010", TbUtils.intLongToRadixString(58L, MIN_RADIX)); + Assertions.assertEquals("0000000010011110", TbUtils.intLongToRadixString(158L, MIN_RADIX)); + Assertions.assertEquals("00000000000000100000001000000001", TbUtils.intLongToRadixString(131585L, MIN_RADIX)); + Assertions.assertEquals("0111111111111111111111111111111111111111111111111111111111111111", TbUtils.intLongToRadixString(Long.MAX_VALUE, MIN_RADIX)); + Assertions.assertEquals("1000000000000000000000000000000000000000000000000000000000000001", TbUtils.intLongToRadixString(-Long.MAX_VALUE, MIN_RADIX)); + Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToRadixString(-102L, MIN_RADIX)); + Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToRadixString(-13158L, MIN_RADIX)); + Assertions.assertEquals("777777777777777777777", TbUtils.intLongToRadixString(Long.MAX_VALUE, 8)); + Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToRadixString(Long.MIN_VALUE, 8)); + Assertions.assertEquals("9223372036854775807", TbUtils.intLongToRadixString(Long.MAX_VALUE)); + Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToRadixString(Long.MIN_VALUE)); + Assertions.assertEquals("3366", TbUtils.intLongToRadixString(13158L, 16)); + Assertions.assertEquals("FFCC9A", TbUtils.intLongToRadixString(-13158L, 16)); + Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToRadixString(-13158L, 16, true, true)); + + Assertions.assertEquals("0x0400", TbUtils.intLongToRadixString(1024L, 16, true, true)); + Assertions.assertNotEquals("400", TbUtils.intLongToRadixString(1024L, 16)); + Assertions.assertEquals("0xFFFC00", TbUtils.intLongToRadixString(-1024L, 16, true, true)); + Assertions.assertNotEquals("0xFC00", TbUtils.intLongToRadixString(-1024L, 16, true, true)); + + Assertions.assertEquals("hazelnut", TbUtils.intLongToRadixString(1356099454469L, MAX_RADIX)); } @Test @@ -1067,6 +1082,18 @@ public class TbUtilsTest { String actual = TbUtils.hexToBase64(hex); Assertions.assertEquals(expected, actual); } + @Test + public void bytesToHex_Test() { + byte[] bb = {(byte) 0xBB, (byte) 0xAA}; + String expected = "BBAA"; + String actual = TbUtils.bytesToHex(bb); + Assertions.assertEquals(expected, actual); + ExecutionArrayList expectedList = new ExecutionArrayList<>(ctx); + expectedList.addAll(List.of(-69, 83)); + expected = "BB53"; + actual = TbUtils.bytesToHex(expectedList); + Assertions.assertEquals(expected, actual); + } private static List toList(byte[] data) { List result = new ArrayList<>(data.length); From 60f89ba47354c13bc380e04507d38df3a31f549d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 3 Sep 2024 13:23:13 +0300 Subject: [PATCH 13/29] added validation for client id length --- .../rule/engine/mqtt/TbMqttNode.java | 16 +++++++-- .../rule/engine/mqtt/TbMqttNodeTest.java | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index c909a44a27..5d9c9548ad 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -77,6 +77,8 @@ public class TbMqttNode extends TbAbstractExternalNode { this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class); try { this.mqttClient = initClient(ctx); + } catch (TbNodeException e) { + throw e; } catch (Exception e) { throw new TbNodeException(e); } @@ -119,8 +121,7 @@ public class TbMqttNode extends TbAbstractExternalNode { MqttClientConfig config = new MqttClientConfig(getSslContext()); config.setOwnerId(getOwnerId(ctx)); if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { - config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ? - this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId()); + config.setClientId(getClientId(ctx)); } config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); @@ -146,6 +147,17 @@ public class TbMqttNode extends TbAbstractExternalNode { return client; } + private String getClientId(TbContext ctx) throws TbNodeException { + String clientId = this.mqttNodeConfiguration.isAppendClientIdSuffix() ? + this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : + this.mqttNodeConfiguration.getClientId(); + if (clientId.length() > 23) { + throw new TbNodeException("Client ID was too long '" + clientId + "'. " + + "The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true); + } + return clientId; + } + MqttClient getMqttClient(TbContext ctx, MqttClientConfig config) { return MqttClient.create(config, null, ctx.getExternalCallExecutor()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index da568eefbb..90ecba923b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -191,6 +191,42 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { assertThatNoException().isThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))); } + @Test + public void givenClientIdIsTooLong_whenInit_thenThrowsException() { + String invalidClientId = "vhfrbeb38ygwfwrgfwefgterhytjytj"; + mqttNodeConfig.setClientId(invalidClientId); + + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); + + assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) + .isInstanceOf(TbNodeException.class) + .hasMessage("Client ID was too long '" + invalidClientId + "'. " + + "The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @Test + public void givenClientIdIsOkAndAppendClientIdSuffixIsTrue_whenInit_thenClientIdBecomesInvalidAndThrowsException() { + String validClientId = "fertjnhnjj4ge"; + mqttNodeConfig.setClientId("fertjnhnjj4ge"); + mqttNodeConfig.setAppendClientIdSuffix(true); + + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); + String serviceId = "test-service"; + given(ctxMock.getServiceId()).willReturn(serviceId); + + String resultedClientId = validClientId + "_" + serviceId; + assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) + .isInstanceOf(TbNodeException.class) + .hasMessage("Client ID was too long '" + resultedClientId + "'. " + + "The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + @Test public void givenFailedByTimeoutConnectResult_whenInit_thenThrowsException() throws ExecutionException, InterruptedException, TimeoutException { mqttNodeConfig.setHost("localhost"); From ea5519893bfa16b4d1b56fab8a1137cc5012763a Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 3 Sep 2024 14:09:03 +0300 Subject: [PATCH 14/29] fixed error message --- .../main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 5d9c9548ad..912496f39e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -152,7 +152,7 @@ public class TbMqttNode extends TbAbstractExternalNode { this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId(); if (clientId.length() > 23) { - throw new TbNodeException("Client ID was too long '" + clientId + "'. " + + throw new TbNodeException("Client ID is too long '" + clientId + "'. " + "The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true); } return clientId; From 03af2564eb6a2e022a8bc1cacd478ef3b1577aab Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 3 Sep 2024 14:22:29 +0300 Subject: [PATCH 15/29] tbel: refactoring tbUtils with docs #2 --- .../src/main/java/org/thingsboard/script/api/tbel/TbUtils.java | 2 +- .../test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 6651f26cc5..43d69e602e 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -1154,7 +1154,7 @@ public class TbUtils { } public static void raiseError(String message, Object value) { - String msg = value == null ? message : message + " for value " + value; + String msg = value == null ? message : message + " A value of " + value + " is invalid."; throw new RuntimeException(msg); } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 2e36b3df42..950587e2e4 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -891,7 +891,7 @@ public class TbUtilsTest { TbUtils.raiseError(message, value); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { - Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. for value 4")); + Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. A value of 4 is invalid.")); } try { TbUtils.raiseError(message); From b518a1168eb6bdcef6d34974383339ec2c1399af Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 3 Sep 2024 17:08:24 +0300 Subject: [PATCH 16/29] rollback to old api path for backward compatibility --- .../org/thingsboard/server/controller/OAuth2Controller.java | 2 +- ui-ngx/src/app/core/auth/auth.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 261c4705a9..8a8055c4af 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -75,7 +75,7 @@ public class OAuth2Controller extends BaseController { @ApiOperation(value = "Get OAuth2 clients (getOAuth2Clients)", notes = "Get the list of OAuth2 clients " + "to log in with, available for such domain scheme (HTTP or HTTPS) (if x-forwarded-proto request header is present - " + "the scheme is known from it) and domain name and port (port may be known from x-forwarded-port header)") - @PostMapping(value = "/noauth/oauth2/client") + @PostMapping(value = "/noauth/oauth2Clients") public List getOAuth2Clients(HttpServletRequest request, @Parameter(description = "Mobile application package name, to find OAuth2 clients " + "where there is configured mobile application with such package name") diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 6f1c2bc116..5f8a968710 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -224,7 +224,7 @@ export class AuthService { } public loadOAuth2Clients(): Observable> { - const url = '/api/noauth/oauth2/client?platform=' + PlatformType.WEB; + const url = '/api/noauth/oauth2Clients?platform=' + PlatformType.WEB; return this.http.post>(url, null, defaultHttpOptions()).pipe( catchError(err => of([])), From 7adb60e0e6eee0a091a581d3814f72916246ad5d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 3 Sep 2024 18:29:06 +0300 Subject: [PATCH 17/29] fixed error msg in the tests --- .../java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index 90ecba923b..8fa8c387fd 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -201,7 +201,7 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID was too long '" + invalidClientId + "'. " + + .hasMessage("Client ID is too long '" + invalidClientId + "'. " + "The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".") .extracting(e -> ((TbNodeException) e).isUnrecoverable()) .isEqualTo(true); @@ -221,7 +221,7 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { String resultedClientId = validClientId + "_" + serviceId; assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID was too long '" + resultedClientId + "'. " + + .hasMessage("Client ID is too long '" + resultedClientId + "'. " + "The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".") .extracting(e -> ((TbNodeException) e).isUnrecoverable()) .isEqualTo(true); From bf6bd064b7e4eb3fb82bf0db820a4616dd0aad2b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 4 Sep 2024 12:01:13 +0300 Subject: [PATCH 18/29] UI: Add country data to module providers. --- ui-ngx/src/app/shared/shared.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 21c912edf5..0ed819b248 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -223,6 +223,7 @@ import { HexInputComponent } from '@shared/components/color-picker/hex-input.com import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component'; import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component'; +import { CountryData } from '@shared/models/country.models'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -279,7 +280,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) disableTooltipInteractivity: true } }, - TbBreakPointsProvider + TbBreakPointsProvider, + CountryData ], declarations: [ FooterComponent, From 5e513787d7710f462ea09f6b3ba76c977cb441e9 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 4 Sep 2024 14:51:41 +0300 Subject: [PATCH 19/29] UI: Refactoring translate --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 ++ 1 file changed, 2 insertions(+) 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 cc28a4edd3..31ab8a1cf2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3830,6 +3830,8 @@ "open-hint": "Action invoked when user clicks to open component.", "close": "Close", "close-hint": "Action invoked when user clicks to close component.", + "close-state-animation": "Closed state animation", + "close-state-animation-hint": "Whether to enable blinking animation when component is in closed state.", "opened-color": "Opened color", "closed-color": "Closed color", "opened-rotation-angle": "Opened rotation angle", From 3bb9924bde177136579e1cd71c76b6d7d3eb50af Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 4 Sep 2024 14:58:29 +0300 Subject: [PATCH 20/29] UI: Update water stop symbol --- .../src/main/data/json/system/scada_symbols/waterstop.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/waterstop.svg b/application/src/main/data/json/system/scada_symbols/waterstop.svg index e1ed0e0fd5..e3fb963d71 100644 --- a/application/src/main/data/json/system/scada_symbols/waterstop.svg +++ b/application/src/main/data/json/system/scada_symbols/waterstop.svg @@ -25,7 +25,7 @@ }, { "tag": "walve", - "stateRenderFunction": "var opened = ctx.values.opened;\nif (opened) {\n element.attr({fill: ctx.properties.openedColor});\n ctx.api.resetAnimation(element);\n} else {\n element.attr({fill: ctx.properties.closedColor});\n if (ctx.values.criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}\n", + "stateRenderFunction": "var opened = ctx.values.opened;\nif (opened) {\n element.attr({fill: ctx.properties.openedColor});\n ctx.api.resetAnimation(element);\n} else {\n element.attr({fill: ctx.properties.closedColor});\n if (ctx.values.closeAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}\n", "actions": null } ], @@ -134,9 +134,9 @@ "defaultWidgetActionSettings": null }, { - "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "id": "closeAnimation", + "name": "{i18n:scada.symbol.close-state-animation}", + "hint": "{i18n:scada.symbol.close-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", From 0eff9cb38064322bd1cfef92849b5362241e468e Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 4 Sep 2024 16:07:24 +0300 Subject: [PATCH 21/29] UI: Add font and color for water level symbols --- .../left-analog-water-level-meter.svg | 40 ++++++++++++++++++- .../right-analog-water-level-meter.svg | 40 ++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg index b2e47991dc..ae02548602 100644 --- a/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg @@ -43,7 +43,7 @@ }, { "tag": "label", - "stateRenderFunction": "var label = ctx.properties.label;\nif (label) {\n var icon = ctx.properties.icon;\n ctx.api.text(element, ctx.properties.labelText);\n if (!icon) {\n element.transform({translateX: 10});\n }\n element.show();\n} else {\n element.hide();\n}", + "stateRenderFunction": "var label = ctx.properties.label;\nif (label) {\n var icon = ctx.properties.icon;\n var labelTextFont = ctx.properties.labelTextFont;\n var labelTextColor = ctx.properties.labelTextColor;\n ctx.api.font(element, labelTextFont, labelTextColor);\n ctx.api.text(element, ctx.properties.labelText);\n if (!icon) {\n element.transform({translateX: 10});\n }\n element.show();\n} else {\n element.hide();\n}", "actions": null }, { @@ -315,6 +315,44 @@ "max": null, "step": null }, + { + "id": "labelTextFont", + "name": "{i18n:scada.symbol.label}", + "type": "font", + "default": { + "size": 13, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "label", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "labelTextColor", + "name": "{i18n:scada.symbol.label}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "label", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, { "id": "icon", "name": "{i18n:scada.symbol.icon}", diff --git a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg index f8f4ceb4b0..ea90b6cf31 100644 --- a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg @@ -43,7 +43,7 @@ }, { "tag": "label", - "stateRenderFunction": "var label = ctx.properties.label;\nif (label) {\n var icon = ctx.properties.icon;\n ctx.api.text(element, ctx.properties.labelText);\n if (!icon) {\n element.transform({translateX: 10});\n }\n element.show();\n} else {\n element.hide();\n}", + "stateRenderFunction": "var label = ctx.properties.label;\nif (label) {\n var icon = ctx.properties.icon;\n var labelTextFont = ctx.properties.labelTextFont;\n var labelTextColor = ctx.properties.labelTextColor;\n ctx.api.font(element, labelTextFont, labelTextColor);\n ctx.api.text(element, ctx.properties.labelText);\n if (!icon) {\n element.transform({translateX: 10});\n }\n element.show();\n} else {\n element.hide();\n}", "actions": null }, { @@ -315,6 +315,44 @@ "max": null, "step": null }, + { + "id": "labelTextFont", + "name": "{i18n:scada.symbol.label}", + "type": "font", + "default": { + "size": 13, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "label", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "labelTextColor", + "name": "{i18n:scada.symbol.label}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "label", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, { "id": "icon", "name": "{i18n:scada.symbol.icon}", From 1c2d1f8780e5a7d1a14a37e4b02afe1fdc8ce13c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 5 Sep 2024 10:19:05 +0300 Subject: [PATCH 22/29] UI: Improve widget layout settings: Add preserve aspect ratio and resizable options. --- .../core/services/dashboard-utils.service.ts | 7 +- .../add-widget-dialog.component.ts | 2 + .../dashboard-page.component.ts | 2 +- .../widget/widget-config.component.html | 36 ++-- .../widget/widget-config.component.ts | 22 ++- .../home/models/dashboard-component.models.ts | 176 +++++++++++++++--- .../src/app/shared/models/dashboard.models.ts | 2 + ui-ngx/src/app/shared/models/widget.models.ts | 2 + .../assets/locale/locale.constant-en_US.json | 6 +- 9 files changed, 206 insertions(+), 49 deletions(-) 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 53d1df8219..2196969d01 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -345,10 +345,11 @@ export class DashboardUtilsService { return widgetConfig; } - public prepareWidgetForScadaLayout(widget: Widget): Widget { + public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget { const config = widget.config; config.showTitle = false; config.dropShadow = false; + config.preserveAspectRatio = isScada; config.padding = '0'; config.margin = '0'; config.backgroundColor = 'rgba(0,0,0,0)'; @@ -654,7 +655,9 @@ export class DashboardUtilsService { mobileOrder: widget.config.mobileOrder, mobileHeight: widget.config.mobileHeight, mobileHide: widget.config.mobileHide, - desktopHide: widget.config.desktopHide + desktopHide: widget.config.desktopHide, + preserveAspectRatio: widget.config.preserveAspectRatio, + resizable: widget.config.resizable }; if (isUndefined(originalColumns)) { originalColumns = 24; 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 1d2fb7bfcf..7a2b550201 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 @@ -187,6 +187,8 @@ export class AddWidgetDialogComponent extends DialogComponent -
-
- - {{ 'widget-config.mobile-hide' | translate }} - -
-
- - {{ 'widget-config.desktop-hide' | translate }} - +
+
+
widget-config.resize-options
+
+ + {{ 'widget-config.resizable' | translate }} + +
+
+ + {{ 'widget-config.preserve-aspect-ratio' | translate }} + +
-
+
+
{{ (isDefaultBreakpoint ? 'widget-config.mobile' : 'widget-config.list-layout') | translate }}
+
+ + {{ 'widget-config.mobile-hide' | translate }} + +
+
+ + {{ 'widget-config.desktop-hide' | translate }} + +
widget-config.order
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 f554c4bf3f..643a5da990 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 @@ -256,6 +256,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe }); this.layoutSettings = this.fb.group({ + resizable: [true], + preserveAspectRatio: [false], mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]], mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]], mobileHide: [false], @@ -349,16 +351,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe value: 'actions' } ); - if (this.showLayoutConfig) { - this.headerOptions.push( - { - name: this.isDefaultBreakpoint - ? this.translate.instant('widget-config.mobile') - : this.translate.instant('widget-config.list-layout'), - value: 'mobile' - } - ); - } + this.headerOptions.push( + { + name: this.translate.instant('widget-config.layout'), + value: 'layout' + } + ); if (!this.selectedOption || !this.headerOptions.find(o => o.value === this.selectedOption)) { this.selectedOption = this.headerOptions[0].value; } @@ -569,6 +567,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe if (layout) { this.layoutSettings.patchValue( { + resizable: isDefined(layout.resizable) ? layout.resizable : true, + preserveAspectRatio: layout.preserveAspectRatio, mobileOrder: layout.mobileOrder, mobileHeight: layout.mobileHeight, mobileHide: layout.mobileHide, @@ -579,6 +579,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } else { this.layoutSettings.patchValue( { + resizable: true, + preserveAspectRatio: false, mobileOrder: null, mobileHeight: null, mobileHide: false, diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index e7fdfa78e3..ed9ca5123f 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -343,6 +343,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { private selectedValue = false; private selectedCallback: (selected: boolean) => void = () => {}; + resizableHandles = {} as any; + + resizeEnabled = true; + isFullscreen = false; isReference = false; @@ -387,6 +391,15 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { private gridsterItemComponentSubject = new Subject(); private gridsterItemComponentValue: GridsterItemComponentInterface; + private readonly aspectRatio: number; + + private heightValue: number; + private widthValue: number; + + private rowsValue: number; + private colsValue: number; + + get mobileHide(): boolean { return this.widgetLayout ? this.widgetLayout.mobileHide === true : false; } @@ -397,10 +410,96 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { set gridsterItemComponent(item: GridsterItemComponentInterface) { this.gridsterItemComponentValue = item; + + if (this.widgetLayout?.preserveAspectRatio) { + this.applyPreserveAspectRatio(item); + } + this.gridsterItemComponentSubject.next(this.gridsterItemComponentValue); this.gridsterItemComponentSubject.complete(); } + private applyPreserveAspectRatio(item: GridsterItemComponentInterface) { + this.resizableHandles.ne = false; + this.resizableHandles.sw = false; + this.resizableHandles.nw = false; + + const $item = item.$item; + + this.rowsValue = $item.rows; + this.colsValue = $item.cols; + + Object.defineProperty($item, 'rows', { + get: () => this.rowsValue, + set: v => { + if (this.rowsValue !== v) { + if (this.preserveAspectRatio) { + this.colsValue = v * this.aspectRatio; + } + this.rowsValue = v; + } + } + }); + + Object.defineProperty($item, 'cols', { + get: () => this.colsValue, + set: v => { + if (this.colsValue !== v) { + if (this.preserveAspectRatio) { + this.rowsValue = v / this.aspectRatio; + } + this.colsValue = v; + } + } + }); + + const resizable = item.resize; + + this.heightValue = resizable.height; + this.widthValue = resizable.width; + + const setItemHeight = resizable.setItemHeight.bind(resizable); + const setItemWidth = resizable.setItemWidth.bind(resizable); + resizable.setItemHeight = (height) => { + setItemHeight(height); + this.heightValue = height; + if (this.preserveAspectRatio) { + setItemWidth(height * this.aspectRatio); + } + }; + resizable.setItemWidth = (width) => { + setItemWidth(width); + this.widthValue = width; + if (this.preserveAspectRatio) { + setItemHeight(width / this.aspectRatio); + } + }; + + Object.defineProperty(resizable, 'height', { + get: () => this.heightValue, + set: v => { + if (this.heightValue !== v) { + if (this.preserveAspectRatio) { + this.widthValue = v * this.aspectRatio; + } + this.heightValue = v; + } + } + }); + + Object.defineProperty(resizable, 'width', { + get: () => this.widthValue, + set: v => { + if (this.widthValue !== v) { + if (this.preserveAspectRatio) { + this.heightValue = v / this.aspectRatio; + } + this.widthValue = v; + } + } + }); + } + get highlighted() { return this.highlightedValue; } @@ -434,6 +533,13 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { public widgetLayout?: WidgetLayout, private parentDashboard?: IDashboardComponent, private popoverComponent?: TbPopoverComponent) { + + if (isDefined(widgetLayout?.resizable)) { + this.resizeEnabled = widgetLayout.resizable; + } + if (widgetLayout?.preserveAspectRatio) { + this.aspectRatio = this.widgetLayout.sizeX / this.widgetLayout.sizeY; + } if (!widget.id) { widget.id = guid(); } @@ -603,30 +709,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { } } - @enumerable(true) - get cols(): number { - let res; - if (this.widgetLayout) { - res = this.widgetLayout.sizeX; + get preserveAspectRatio(): boolean { + if (!this.dashboard.isMobileSize && this.widgetLayout) { + return this.widgetLayout.preserveAspectRatio; } else { - res = this.widget.sizeX; + return false; } - return Math.floor(res); + } + + @enumerable(true) + get cols(): number { + return Math.floor(this.sizeX); } set cols(cols: number) { if (!this.dashboard.isMobileSize) { - if (this.widgetLayout) { - this.widgetLayout.sizeX = cols; - } else { - this.widget.sizeX = cols; - } + this.sizeX = cols; } } @enumerable(true) get rows(): number { - let res; + let res: number; if (this.dashboard.isMobileSize) { let mobileHeight; if (this.widgetLayout) { @@ -638,26 +742,50 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { if (mobileHeight) { res = mobileHeight; } else { - const sizeY = this.widgetLayout ? this.widgetLayout.sizeY : this.widget.sizeY; + const sizeY = this.sizeY; res = sizeY * 24 / this.dashboard.gridsterOpts.minCols; } } else { - if (this.widgetLayout) { - res = this.widgetLayout.sizeY; - } else { - res = this.widget.sizeY; - } + res = this.sizeY; } return Math.floor(res); } set rows(rows: number) { if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) { - if (this.widgetLayout) { - this.widgetLayout.sizeY = rows; - } else { - this.widget.sizeY = rows; - } + this.sizeY = rows; + } + } + + get sizeX(): number { + if (this.widgetLayout) { + return this.widgetLayout.sizeX; + } else { + return this.widget.sizeX; + } + } + + set sizeX(sizeX: number) { + if (this.widgetLayout) { + this.widgetLayout.sizeX = sizeX; + } else { + this.widget.sizeX = sizeX; + } + } + + get sizeY(): number { + if (this.widgetLayout) { + return this.widgetLayout.sizeY; + } else { + return this.widget.sizeY; + } + } + + set sizeY(sizeY: number) { + if (this.widgetLayout) { + this.widgetLayout.sizeY = sizeY; + } else { + this.widget.sizeY = sizeY; } } diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 2621175030..c4e767b314 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -43,6 +43,8 @@ export interface WidgetLayout { mobileOrder?: number; col?: number; row?: number; + resizable?: boolean; + preserveAspectRatio?: boolean; } export interface WidgetLayouts { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 3b60cd727e..429bb79e35 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -761,6 +761,8 @@ export interface WidgetConfig { displayTimewindow?: boolean; timewindow?: Timewindow; timewindowStyle?: TimewindowStyle; + resizable?: boolean; + preserveAspectRatio?: boolean; desktopHide?: boolean; mobileHide?: boolean; mobileHeight?: number; 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 4dd7125f72..aaa51ea3dd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6045,7 +6045,11 @@ "color": "Color", "tooltip": "Tooltip", "units-required": "Unit is required.", - "list-layout": "List layout" + "list-layout": "List layout", + "layout": "Layout", + "resize-options": "Resize options", + "resizable": "Resizable", + "preserve-aspect-ratio": "Preserve aspect ratio" }, "widget-type": { "import": "Import widget type", From 7a84dc655dbff28805e54061d14275f375c884f8 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 5 Sep 2024 12:23:43 +0300 Subject: [PATCH 23/29] added catch for DataValidationException --- .../engine/aws/lambda/TbAwsLambdaNode.java | 5 ++++- .../aws/lambda/TbAwsLambdaNodeTest.java | 22 ++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index 06dc0310ae..1dbc81bfa1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; @@ -65,8 +66,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class); String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; - validateFields(config, errorPrefix); try { + validateFields(config, errorPrefix); AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()); client = AWSLambdaAsyncClientBuilder.standard() .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) @@ -75,6 +76,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { .withConnectionTimeout((int) TimeUnit.SECONDS.toMillis(config.getConnectionTimeout())) .withRequestTimeout((int) TimeUnit.SECONDS.toMillis(config.getRequestTimeout()))) .build(); + } catch (DataValidationException e) { + throw new TbNodeException(e, true); } catch (Exception e) { throw new TbNodeException(e); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java index d730b9b837..14ae9bef20 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java @@ -36,6 +36,7 @@ import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; @@ -43,7 +44,6 @@ 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.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -99,7 +99,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidFunctionName_whenInit_thenThrowsException(String funcName) { config.setFunctionName(funcName); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -107,7 +107,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) { config.setAccessKey(accessKey); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -115,7 +115,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) { config.setSecretKey(secretAccessKey); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -123,19 +123,19 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidRegion_whenInit_thenThrowsException(String region) { config.setRegion(region); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @Test public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() { config.setConnectionTimeout(-100); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @Test public void givenInvalidRequestTimeout_whenInit_thenThrowsException() { config.setRequestTimeout(-100); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -318,14 +318,16 @@ public class TbAwsLambdaNodeTest { assertThat(throwableCaptor.getValue()).isInstanceOf(AWSLambdaException.class).hasMessageStartingWith(errorMsg); } - private void verifyDataValidationExceptionOnInit() { + private void verifyValidationExceptionOnInit() { RuleNode ruleNode = new RuleNode(); ruleNode.setName("test"); when(ctx.getSelf()).thenReturn(ruleNode); String errorPrefix = "'test' node configuration is invalid: "; assertThatThrownBy(() -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) - .isInstanceOf(DataValidationException.class) - .hasMessageContaining(errorPrefix); + .isInstanceOf(TbNodeException.class) + .hasMessageContaining(errorPrefix) + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); } private void init() { From 6f2801db0aab0b901524196cc2040f3ff6b3d955 Mon Sep 17 00:00:00 2001 From: Iryna Matveieva <101514424+irynamatveieva@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:38:52 +0300 Subject: [PATCH 24/29] Fixed incorrect display of device state (#11536) * changed logic to save to db activity value from cache * changed check if partition belongs --- .../state/DefaultDeviceStateService.java | 63 +++++----- .../state/DefaultDeviceStateServiceTest.java | 108 ++++++++++++++++++ 2 files changed, 141 insertions(+), 30 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 1c53920a93..6025e874b9 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -157,7 +157,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService() { @Override - public void onSuccess(@Nullable DeviceStateData state) { + public void onSuccess(DeviceStateData state) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, device.getId()); - if (addDeviceUsingState(tpi, state)) { - save(deviceId, ACTIVITY_STATE, false); + Set deviceIds = partitionedEntities.get(tpi); + boolean isMyPartition = deviceIds != null; + if (isMyPartition) { + deviceIds.add(state.getDeviceId()); + initializeActivityState(deviceId, state); callback.onSuccess(); } else { - log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}" - , tenantId, deviceId, tpi.getFullTopicName()); + log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}", tenantId, deviceId, tpi.getFullTopicName()); callback.onFailure(new RuntimeException("Device belongs to external partition " + tpi.getFullTopicName() + "!")); } } @@ -400,6 +403,21 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIdSet = partitionedEntities.get(tpi); + if (deviceIdSet != null) { + deviceIdSet.remove(deviceId); + } + } + + private void initializeActivityState(DeviceId deviceId, DeviceStateData fetchedState) { + DeviceStateData cachedState = deviceStates.putIfAbsent(fetchedState.getDeviceId(), fetchedState); + boolean activityState = Objects.requireNonNullElse(cachedState, fetchedState).getState().isActive(); + save(deviceId, ACTIVITY_STATE, activityState); + } + @Override protected Map>> onAddedPartitions(Set addedPartitions) { var result = new HashMap>>(); @@ -436,10 +454,16 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIds = partitionedEntities.get(tpi); + boolean isMyPartition = deviceIds != null; + if (isMyPartition) { + deviceIds.add(state.getDeviceId()); + deviceStates.putIfAbsent(state.getDeviceId(), state); + checkAndUpdateState(state.getDeviceId(), state); + } else { + log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName()); } - checkAndUpdateState(state.getDeviceId(), state); } log.info("[{}] Initialized {} out of {} device states", entry.getKey().getPartition().orElse(0), counter.addAndGet(states.size()), entry.getValue().size()); } @@ -475,18 +499,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIds = partitionedEntities.get(tpi); - if (deviceIds != null) { - deviceIds.add(state.getDeviceId()); - deviceStates.putIfAbsent(state.getDeviceId(), state); - return true; - } else { - log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName()); - return false; - } - } - void checkStates() { try { final long ts = getCurrentTimeMillis(); @@ -619,15 +631,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIdSet = partitionedEntities.get(tpi); - if (deviceIdSet != null) { - deviceIdSet.remove(deviceId); - } - } - @Override protected void cleanupEntityOnPartitionRemoval(DeviceId deviceId) { cleanupEntity(deviceId); diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 9833396a0a..296191d61b 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.state; +import com.google.common.util.concurrent.Futures; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,8 +29,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; @@ -41,11 +44,13 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.sql.query.EntityQueryRepository; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; @@ -66,6 +71,7 @@ import java.util.stream.Stream; 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.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -1070,4 +1076,106 @@ public class DefaultDeviceStateServiceTest { then(service).should().fetchDeviceStateDataUsingSeparateRequests(deviceId); } + @Test + public void givenDeviceAdded_whenOnQueueMsg_thenShouldCacheAndSaveActivityToFalse() throws InterruptedException { + // GIVEN + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + + TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setAdded(true) + .setUpdated(false) + .setDeleted(false) + .build(); + + // WHEN + service.onQueueMsg(proto, TbCallback.EMPTY); + + // THEN + await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(false); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(false), any()); + }); + } + + @Test + public void givenDeviceActivityEventHappenedAfterAdded_whenOnDeviceActivity_thenShouldCacheAndSaveActivityToTrue() throws InterruptedException { + // GIVEN + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + long currentTime = System.currentTimeMillis(); + DeviceState deviceState = DeviceState.builder() + .active(false) + .inactivityTimeout(service.getDefaultInactivityTimeoutInSec()) + .build(); + DeviceStateData stateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .deviceCreationTime(currentTime - 10000) + .state(deviceState) + .metaData(TbMsgMetaData.EMPTY) + .build(); + service.deviceStates.put(deviceId, stateData); + + // WHEN + service.onDeviceActivity(tenantId, deviceId, currentTime); + + // THEN + await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(LAST_ACTIVITY_TIME), eq(currentTime), any()); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any()); + }); + } + + @Test + public void givenDeviceActivityEventHappenedBeforeAdded_whenOnQueueMsg_thenShouldSaveActivityStateUsingValueFromCache() throws InterruptedException { + // GIVEN + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + + long currentTime = System.currentTimeMillis(); + DeviceState deviceState = DeviceState.builder() + .active(true) + .lastConnectTime(currentTime - 8000) + .lastActivityTime(currentTime - 4000) + .lastDisconnectTime(0) + .lastInactivityAlarmTime(0) + .inactivityTimeout(3000) + .build(); + DeviceStateData stateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .deviceCreationTime(currentTime - 10000) + .state(deviceState) + .build(); + service.deviceStates.put(deviceId, stateData); + + // WHEN + TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setAdded(true) + .setUpdated(false) + .setDeleted(false) + .build(); + service.onQueueMsg(proto, TbCallback.EMPTY); + + // THEN + await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any()); + }); + } + } From cfcf627fe10de244ca69711859c3a14d4792ee8a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 5 Sep 2024 12:48:55 +0300 Subject: [PATCH 25/29] UI: Improve widget resizable options. --- .../core/services/dashboard-utils.service.ts | 1 + .../add-widget-dialog.component.ts | 9 +++++++- .../widget/widget-config.component.ts | 23 +++++++++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) 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 2196969d01..5c2363789d 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -349,6 +349,7 @@ export class DashboardUtilsService { const config = widget.config; config.showTitle = false; config.dropShadow = false; + config.resizable = !isScada; config.preserveAspectRatio = isScada; config.padding = '0'; config.margin = '0'; 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 7a2b550201..709159aec3 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 @@ -130,7 +130,14 @@ export class AddWidgetDialogComponent extends DialogComponent { - this.updateWidgetSettingsEnabledState(); - }); - this.widgetSettings.get('showTitleIcon').valueChanges.subscribe(() => { + merge(this.widgetSettings.get('showTitle').valueChanges, + this.widgetSettings.get('showTitleIcon').valueChanges).subscribe(() => { this.updateWidgetSettingsEnabledState(); }); @@ -263,6 +261,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe mobileHide: [false], desktopHide: [false] }); + + this.layoutSettings.get('resizable').valueChanges.subscribe(() => { + this.updateLayoutEnabledState(); + }); + this.actionsSettings = this.fb.group({ actions: [null, []] }); @@ -589,6 +592,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe {emitEvent: false} ); } + this.updateLayoutEnabledState(); } this.createChangeSubscriptions(); } @@ -622,6 +626,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } } + private updateLayoutEnabledState() { + const resizable: boolean = this.layoutSettings.get('resizable').value; + if (resizable) { + this.layoutSettings.get('preserveAspectRatio').enable({emitEvent: false}); + } else { + this.layoutSettings.get('preserveAspectRatio').disable({emitEvent: false}); + } + } + private updateSchemaForm(settings?: any) { const widgetSettingsFormData: JsonFormComponentData = {}; if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) { From b226ebdccfed54a99fa725b39002a0b3694c5644 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 5 Sep 2024 15:15:32 +0300 Subject: [PATCH 26/29] TbHttpClient - added check for maxInMemoryBufferSizeInKb; should be less than system configured maxInMemoryBufferSizeInKb --- .../rule/engine/rest/TbHttpClient.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 839a7633fc..c21914577f 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 @@ -80,6 +80,8 @@ public class TbHttpClient { public static final String PROXY_USER = "tb.proxy.user"; public static final String PROXY_PASSWORD = "tb.proxy.password"; + public static final String MAX_IN_MEMORY_BUFFER_SIZE_IN_KB = "tb.http.maxInMemoryBufferSizeInKb"; + private final TbRestApiCallNodeConfiguration config; private EventLoopGroup eventLoopGroup; @@ -129,6 +131,8 @@ public class TbHttpClient { httpClient = httpClient.secure(t -> t.sslContext(sslContext)); } + validateMaxInMemoryBufferSize(config); + this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset") @@ -140,6 +144,21 @@ public class TbHttpClient { } } + private void validateMaxInMemoryBufferSize(TbRestApiCallNodeConfiguration config) throws TbNodeException { + int systemMaxInMemoryBufferSizeInKb = 25000; + try { + Properties properties = System.getProperties(); + if (properties.containsKey(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB)) { + systemMaxInMemoryBufferSizeInKb = Integer.parseInt(properties.getProperty(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB)); + } + } catch (Exception ignored) {} + if (config.getMaxInMemoryBufferSizeInKb() > systemMaxInMemoryBufferSizeInKb) { + throw new TbNodeException("The configured maximum in-memory buffer size (in KB) exceeds the system limit for this parameter.\n" + + "The system limit is " + systemMaxInMemoryBufferSizeInKb + " KB.\n" + + "Please use the system variable '" + MAX_IN_MEMORY_BUFFER_SIZE_IN_KB + "' to override the system limit."); + } + } + EventLoopGroup getSharedOrCreateEventLoopGroup(EventLoopGroup eventLoopGroupShared) { if (eventLoopGroupShared != null) { return eventLoopGroupShared; From 5699c49c535923c85af31068936a1ae7aea66aa1 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 5 Sep 2024 18:49:53 +0300 Subject: [PATCH 27/29] tbel: refactoring raiseError --- .../java/org/thingsboard/script/api/tbel/TbUtils.java | 9 +-------- .../org/thingsboard/script/api/tbel/TbUtilsTest.java | 7 +++++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 43d69e602e..7f7b1f699f 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -315,8 +315,6 @@ public class TbUtils { String.class))); parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI", String.class))); - parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError", - String.class, Object.class))); parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError", String.class))); parserConfig.addImport("isBinary", new MethodStub(TbUtils.class.getMethod("isBinary", @@ -1150,12 +1148,7 @@ public class TbUtils { } public static void raiseError(String message) { - raiseError(message, null); - } - - public static void raiseError(String message, Object value) { - String msg = value == null ? message : message + " A value of " + value + " is invalid."; - throw new RuntimeException(msg); + throw new RuntimeException(message); } private static void parseRecursive(Object json, Map map, List excludeList, String path, boolean pathInKey) { diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 950587e2e4..b7276edd8d 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -19,6 +19,7 @@ import com.google.common.collect.Lists; import com.google.common.primitives.Bytes; import com.google.common.primitives.Ints; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.units.qual.A; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -885,14 +886,16 @@ public class TbUtilsTest { @Test public void raiseError_Test() { - String message = "frequency_weighting_type must be 0, 1 or 2."; Object value = 4; + String message = "frequency_weighting_type must be 0, 1 or 2. A value of " + value.toString() + " is invalid."; + try { - TbUtils.raiseError(message, value); + TbUtils.raiseError(message); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. A value of 4 is invalid.")); } + message = "frequency_weighting_type must be 0, 1 or 2."; try { TbUtils.raiseError(message); Assertions.fail("Should throw NumberFormatException"); From 27fa09037f0c688f36f05ef1f270471dc9c1cafa Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 6 Sep 2024 12:12:05 +0300 Subject: [PATCH 28/29] UI: Fixed right analog water meter symbol --- .../system/scada_symbols/right-analog-water-level-meter.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg index ea90b6cf31..8658a6dc3e 100644 --- a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg @@ -638,7 +638,7 @@ - + @@ -660,7 +660,7 @@ - + From 174166d9a127d03affde9de3cdc4ee6676d70f1d Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 6 Sep 2024 18:18:10 +0300 Subject: [PATCH 29/29] SCADA Symbol editor: Add XML editor mode. --- ui-ngx/angular.json | 5 + .../widget/lib/scada/scada-symbol.models.ts | 23 +- .../scada-symbol-metadata-tags.component.html | 2 +- .../scada-symbol-editor.component.html | 25 ++- .../scada-symbol-editor.component.scss | 10 +- .../scada-symbol-editor.component.ts | 75 ++++++- .../scada-symbol-editor.models.ts | 1 + .../scada-symbol/scada-symbol.component.html | 6 +- .../scada-symbol/scada-symbol.component.ts | 8 + .../shared/components/svg-xml.component.html | 38 ++++ .../shared/components/svg-xml.component.scss | 85 +++++++ .../shared/components/svg-xml.component.ts | 211 ++++++++++++++++++ .../components/toggle-header.component.html | 31 ++- .../components/toggle-header.component.scss | 38 ++++ .../components/toggle-header.component.ts | 12 + .../components/toggle-select.component.html | 4 + .../components/toggle-select.component.scss | 22 ++ .../components/toggle-select.component.ts | 14 +- .../src/app/shared/models/ace/ace.models.ts | 4 + ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 2 + 21 files changed, 584 insertions(+), 35 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/svg-xml.component.html create mode 100644 ui-ngx/src/app/shared/components/svg-xml.component.scss create mode 100644 ui-ngx/src/app/shared/components/svg-xml.component.ts create mode 100644 ui-ngx/src/app/shared/components/toggle-select.component.scss diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 36b2b4b2f1..10c904f88d 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -39,6 +39,11 @@ "input": "./node_modules/ace-builds/src-noconflict/", "output": "/" }, + { + "glob": "worker-xml.js", + "input": "./node_modules/ace-builds/src-noconflict/", + "output": "/" + }, { "glob": "worker-css.js", "input": "./node_modules/ace-builds/src-noconflict/", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 3c4e32abf7..ff74bc58ac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -289,13 +289,24 @@ const updateScadaSymbolMetadataInDom = (svgDoc: Document, metadata: ScadaSymbolM metadataElement.appendChild(cdata); }; -const tbMetadataRegex = /.*<\/tb:metadata>/gs; +const tbMetadataRegex = /]*>.*<\/tb:metadata>/gs; export interface ScadaSymbolContentData { svgRootNode: string; innerSvg: string; } +export const removeScadaSymbolMetadata = (svgContent: string): string => { + let result = svgContent; + tbMetadataRegex.lastIndex = 0; + const metadataMatch = tbMetadataRegex.exec(svgContent); + if (metadataMatch !== null && metadataMatch.length) { + const metadata = metadataMatch[0]; + result = result.replace(metadata, ''); + } + return result; +}; + export const scadaSymbolContentData = (svgContent: string): ScadaSymbolContentData => { const result: ScadaSymbolContentData = { svgRootNode: '', @@ -517,10 +528,12 @@ export class ScadaSymbolObject { if (this.context) { for (const tag of this.metadata.tags) { const elements = this.context.tags[tag.tag]; - elements.forEach(element => { - element.timeline().stop(); - element.timeline(null); - }); + if (elements) { + elements.forEach(element => { + element.timeline().stop(); + element.timeline(null); + }); + } } } if (this.svgShape) { 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 ec0f69a7f0..32bb345ecf 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 @@ -35,6 +35,6 @@
- {{ '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 4eaaf6d718..96bd1d7a8a 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 @@ -15,13 +15,19 @@ limitations under the License. --> -
-
+
+
+
+ + +
-
+
-
+
-
+
+ + {{ 'scada.mode-svg' | translate }} + {{ 'scada.mode-xml' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss index c400a32fef..b8a16f4c81 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss @@ -25,6 +25,11 @@ inset: 0; pointer-events: none; } +.tb-scada-symbol-editor-svg-xml { + width: 100%; + height: 100%; + padding-top: 88px; +} .tb-scada-symbol-editor-buttons { pointer-events: none; position: absolute; @@ -64,11 +69,14 @@ } } - .tb-scada-symbol-editor-upload-buttons { + .tb-scada-symbol-editor-right-buttons { display: flex; flex-direction: row; gap: 8px; z-index: 101; + tb-toggle-select { + pointer-events: auto; + } } } 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 9228c3e561..97e9305470 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 @@ -34,11 +34,15 @@ import { ScadaSymbolEditObjectCallbacks } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; +import { FormControl } from '@angular/forms'; +import { removeScadaSymbolMetadata } from '@home/components/widget/lib/scada/scada-symbol.models'; export interface ScadaSymbolEditorData { scadaSymbolContent: string; } +type editorModeType = 'svg' | 'xml'; + @Component({ selector: 'tb-scada-symbol-editor', templateUrl: './scada-symbol-editor.component.html', @@ -84,10 +88,31 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI displayShowHidden = false; + svgContentFormControl = new FormControl(); + + svgContent: string; + + private editorModeValue: editorModeType = 'svg'; + + get editorMode(): editorModeType { + return this.editorModeValue; + } + + set editorMode(value: editorModeType) { + this.updateEditorMode(value); + } + constructor(private cd: ChangeDetectorRef) { } ngOnInit(): void { + this.svgContentFormControl.valueChanges.subscribe((svgContent) => { + if (this.svgContent !== svgContent) { + this.svgContent = svgContent; + this.editObjectCallbacks.onSymbolEditObjectDirty(true); + } + this.editObjectCallbacks.onSymbolEditObjectValid(this.svgContentFormControl.valid); + }); } ngAfterViewInit() { @@ -114,13 +139,16 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { if (propName === 'data') { - if (this.scadaSymbolEditObject) { - setTimeout(() => { - this.updateContent(this.data.scadaSymbolContent); - }); - } + setTimeout(() => { + this.updateContent(this.data.scadaSymbolContent); + }); } else if (propName === 'readonly') { this.scadaSymbolEditObject.setReadOnly(this.readonly); + if (this.readonly) { + this.svgContentFormControl.disable({emitEvent: false}); + } else { + this.svgContentFormControl.enable({emitEvent: false}); + } } } } @@ -131,7 +159,11 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI } getContent(): string { - return this.scadaSymbolEditObject?.getContent(); + if (this.editorMode === 'svg') { + return this.scadaSymbolEditObject?.getContent(); + } else { + return this.svgContent; + } } zoomIn() { @@ -148,12 +180,33 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI this.scadaSymbolEditObject.showHiddenElements(this.showHiddenElements); } + private updateEditorMode(mode: editorModeType) { + this.editorModeValue = mode; + if (mode === 'xml') { + this.svgContent = this.scadaSymbolEditObject.getContent(); + this.svgContentFormControl.setValue(this.svgContent, {emitEvent: false}); + } else { + this.updateEditObjectContent(this.svgContent); + } + } + private updateContent(content: string) { - this.displayShowHidden = false; - this.scadaSymbolEditObject.setContent(content); - setTimeout(() => { - this.updateZoomButtonsState(); - }); + this.svgContent = removeScadaSymbolMetadata(content); + if (this.editorMode === 'xml') { + this.svgContentFormControl.setValue(this.svgContent, {emitEvent: false}); + } else { + this.updateEditObjectContent(this.svgContent); + } + } + + private updateEditObjectContent(content: string) { + if (this.scadaSymbolEditObject) { + this.displayShowHidden = false; + this.scadaSymbolEditObject.setContent(content); + setTimeout(() => { + this.updateZoomButtonsState(); + }); + } } private updateZoomButtonsState() { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts index f3caf1dbfc..ddc935d202 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts @@ -54,6 +54,7 @@ export interface ScadaSymbolEditObjectCallbacks { tagsUpdated: (tags: string[]) => void; hasHiddenElements?: (hasHidden: boolean) => void; onSymbolEditObjectDirty: (dirty: boolean) => void; + onSymbolEditObjectValid: (valid: boolean) => void; onZoom?: () => void; } 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 6662509fa6..abd015731e 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 @@ -85,7 +85,7 @@ formControlName="metadata"> + +
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/svg-xml.component.scss b/ui-ngx/src/app/shared/components/svg-xml.component.scss new file mode 100644 index 0000000000..8ccb5f1f7d --- /dev/null +++ b/ui-ngx/src/app/shared/components/svg-xml.component.scss @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2024 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. + */ +.tb-svg-xml { + position: relative; + + &.tb-disabled { + color: rgba(0, 0, 0, .38); + } + + &.fill-height { + height: 100%; + } + + &.no-label { + .tb-svg-xml-content-panel { + height: 100%; + } + .tb-svg-xml-toolbar { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 10; + pointer-events: none; + button { + pointer-events: auto; + } + } + } + + .tb-svg-xml-content-panel { + height: calc(100% - 40px); + border: 1px solid #c0c0c0; + + #tb-svg-xml-input { + width: 100%; + min-width: 200px; + height: 100%; + } + } + + &:not(.tb-fullscreen) { + padding-bottom: 15px; + } + + .tb-svg-xml-toolbar { + & > * { + &:not(:last-child) { + margin-right: 4px; + } + } + button.mat-mdc-button-base, button.mat-mdc-button-base.tb-mat-32 { + background: rgba(220, 220, 220, .35); + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + font-size: .8rem; + line-height: 15px; + &:not(.tb-help-popup-button) { + color: #7b7b7b; + } + } + button.mat-mdc-button-base:not(.mat-mdc-icon-button) { + height: 23px; + } + .tb-help-popup-button-loading { + background: #f3f3f3; + } + } +} diff --git a/ui-ngx/src/app/shared/components/svg-xml.component.ts b/ui-ngx/src/app/shared/components/svg-xml.component.ts new file mode 100644 index 0000000000..ce57b8d998 --- /dev/null +++ b/ui-ngx/src/app/shared/components/svg-xml.component.ts @@ -0,0 +1,211 @@ +/// +/// Copyright © 2016-2024 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 { + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import { Ace } from 'ace-builds'; +import { getAce } from '@shared/models/ace/ace.models'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-svg-xml', + templateUrl: './svg-xml.component.html', + styleUrls: ['./svg-xml.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SvgXmlComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SvgXmlComponent), + multi: true, + } + ], + encapsulation: ViewEncapsulation.None +}) +export class SvgXmlComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { + + @ViewChild('svgXmlEditor', {static: true}) + svgXmlEditorElmRef: ElementRef; + + private svgXmlEditor: Ace.Editor; + private editorsResizeCaf: CancelAnimationFrame; + private editorResize$: ResizeObserver; + private ignoreChange = false; + + @Input() label: string; + + @Input() disabled: boolean; + + @Input() + @coerceBoolean() + fillHeight = false; + + @Input() + @coerceBoolean() + noLabel = false; + + @Input() minHeight = '200px'; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + fullscreen = false; + + modelValue: string; + + hasErrors = false; + + private propagateChange = null; + + constructor(public elementRef: ElementRef, + private utils: UtilsService, + private translate: TranslateService, + protected store: Store, + private raf: RafService, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + const editorElement = this.svgXmlEditorElmRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/svg', + showGutter: true, + showPrintMargin: true, + readOnly: this.disabled + }; + + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + getAce().subscribe( + (ace) => { + this.svgXmlEditor = ace.edit(editorElement, editorOptions); + this.svgXmlEditor.session.setUseWrapMode(true); + this.svgXmlEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.svgXmlEditor.setReadOnly(this.disabled); + this.svgXmlEditor.on('change', () => { + if (!this.ignoreChange) { + this.updateView(); + } + }); + // @ts-ignore + this.svgXmlEditor.session.on('changeAnnotation', () => { + const annotations = this.svgXmlEditor.session.getAnnotations(); + const hasErrors = annotations.filter(annotation => annotation.type === 'error').length > 0; + if (this.hasErrors !== hasErrors) { + this.hasErrors = hasErrors; + this.propagateChange(this.modelValue); + this.cd.markForCheck(); + } + }); + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); + } + ); + } + + ngOnDestroy(): void { + if (this.editorResize$) { + this.editorResize$.disconnect(); + } + if (this.svgXmlEditor) { + this.svgXmlEditor.destroy(); + } + } + + private onAceEditorResize() { + if (this.editorsResizeCaf) { + this.editorsResizeCaf(); + this.editorsResizeCaf = null; + } + this.editorsResizeCaf = this.raf.raf(() => { + this.svgXmlEditor.resize(); + this.svgXmlEditor.renderer.updateFull(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.svgXmlEditor) { + this.svgXmlEditor.setReadOnly(this.disabled); + } + } + + public validate(c: UntypedFormControl) { + return (!this.hasErrors) ? null : { + svgXml: { + valid: false, + }, + }; + } + + writeValue(value: string): void { + this.modelValue = value; + if (this.svgXmlEditor) { + this.ignoreChange = true; + this.svgXmlEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.ignoreChange = false; + } + } + + updateView() { + const editorValue = this.svgXmlEditor.getValue(); + if (this.modelValue !== editorValue) { + this.modelValue = editorValue; + this.propagateChange(this.modelValue); + this.cd.markForCheck(); + } + } +} 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 225ccfe141..ff85b45de1 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -20,16 +20,26 @@ [disabled]="!leftPaginationEnabled" (click)="handlePaginatorClick('before', $event)" (touchstart)="handlePaginatorTouchStart('before', $event)" - class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': !isMdLg, 'tb-mat-24': isMdLg}"> + class="tb-toggle-header-pagination-button" + [class.fill-height]="fillHeight" + [class.tb-mat-32]="!isMdLg" + [class.tb-mat-24]="isMdLg"> chevron_left -
+
{{ option.name }} @@ -39,11 +49,16 @@ [disabled]="!rightPaginationEnabled" (click)="handlePaginatorClick('after', $event)" (touchstart)="handlePaginatorTouchStart('after', $event)" - class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': !isMdLg, 'tb-mat-24': isMdLg}"> + class="tb-toggle-header-pagination-button" + [class.fill-height]="fillHeight" + [class.tb-mat-32]="!isMdLg" + [class.tb-mat-24]="isMdLg"> chevron_right - + {{ option.name }} diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.scss b/ui-ngx/src/app/shared/components/toggle-header.component.scss index 0c59f4f919..2de4e14209 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.scss +++ b/ui-ngx/src/app/shared/components/toggle-header.component.scss @@ -21,6 +21,9 @@ grid-template-columns: min-content minmax(auto, 1fr) min-content; .tb-toggle-header-pagination-button { display: none; + &.fill-height { + height: 100%; + } } &.tb-toggle-header-pagination-controls-enabled { .tb-toggle-header-pagination-button { @@ -43,6 +46,25 @@ } :host ::ng-deep { + .tb-toggle-container { + &.fill-height { + .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { + height: 100%; + .mat-button-toggle.mat-button-toggle-appearance-standard { + .mat-button-toggle-button { + height: 100%; + } + } + } + } + &.extra-padding { + .mat-button-toggle.mat-button-toggle-appearance-standard { + .mat-button-toggle-label-content { + padding: 0 20px; + } + } + } + } .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { overflow: visible; width: 100%; @@ -51,6 +73,12 @@ padding: 2px; border: none; background: rgba(0, 0, 0, 0.06); + &.tb-primary-fill { + background: none; + &:before { + border-radius: 100px; + } + } .mat-button-toggle + .mat-button-toggle { border-left: none; } @@ -161,6 +189,16 @@ &.mat-mdc-form-field { line-height: 16px; font-size: 12px; + &.fill-height { + .mat-mdc-text-field-wrapper { + .mat-mdc-form-field-flex { + margin: auto; + } + } + .mat-mdc-form-field-subscript-wrapper { + display: none; + } + } } .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { min-height: 0; diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.ts b/ui-ngx/src/app/shared/components/toggle-header.component.ts index eef0f8bd74..92ccbb76f9 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-header.component.ts @@ -187,6 +187,18 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterV @coerceBoolean() disabled = false; + @Input() + @coerceBoolean() + fillHeight = false; + + @Input() + @coerceBoolean() + extraPadding = false; + + @Input() + @coerceBoolean() + primaryBackground = false; + get isMdLg(): boolean { return !this.ignoreMdLgSize && this.isMdLgValue; } diff --git a/ui-ngx/src/app/shared/components/toggle-select.component.html b/ui-ngx/src/app/shared/components/toggle-select.component.html index c5c762d17f..f0d8172f41 100644 --- a/ui-ngx/src/app/shared/components/toggle-select.component.html +++ b/ui-ngx/src/app/shared/components/toggle-select.component.html @@ -16,10 +16,14 @@ --> { aceObservables.push(from(import('ace-builds/src-noconflict/mode-text'))); aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown'))); aceObservables.push(from(import('ace-builds/src-noconflict/mode-html'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-xml'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-svg'))); aceObservables.push(from(import('ace-builds/src-noconflict/mode-c_cpp'))); aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java'))); @@ -47,6 +49,8 @@ function loadAceDependencies(): Observable { aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/xml'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/svg'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/c_cpp'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/protobuf'))); aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate'))); diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 0ed819b248..2bf3676b32 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -224,6 +224,7 @@ import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component'; import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component'; import { CountryData } from '@shared/models/country.models'; +import { SvgXmlComponent } from '@shared/components/svg-xml.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -338,6 +339,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) JsFuncComponent, CssComponent, HtmlComponent, + SvgXmlComponent, FabTriggerDirective, FabActionsDirective, FabToolbarComponent, @@ -544,6 +546,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) JsFuncComponent, CssComponent, HtmlComponent, + SvgXmlComponent, FabTriggerDirective, FabActionsDirective, TbJsonToStringDirective, 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 0d32107f8d..369985740d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3661,6 +3661,8 @@ "update-symbol": "Update SCADA symbol", "edit-symbol": "Edit SCADA symbol", "symbol-details": "SCADA symbol details", + "mode-svg": "SVG", + "mode-xml": "XML", "no-symbols": "No symbols found", "show-hidden-elements": "Show hidden elements", "hide-hidden-elements": "Hide hidden elements",