Browse Source

Merge remote-tracking branch 'ce/rc' into add-validation-for-edge-rule-chain

pull/13329/head
Yevhenii 1 year ago
parent
commit
2ed8b4fe9d
  1. 63
      application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg
  2. 69
      application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg
  3. 74
      application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg
  4. 112
      application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg
  5. 75
      application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg
  6. 71
      application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg
  7. 63
      application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg
  8. 118
      application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
  9. 119
      application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg
  10. 69
      application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg
  11. 63
      application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg
  12. 69
      application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg
  13. 115
      application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg
  14. 2
      application/src/main/data/json/system/widget_bundles/high_performance_scada_energy_system.json
  15. 7
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  16. 9
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  17. 5
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  18. 26
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
  19. 5
      application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java
  20. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  21. 46
      application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java
  22. 4
      common/cluster-api/pom.xml
  23. 4
      common/dao-api/pom.xml
  24. 3
      common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java
  25. 8
      common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java
  26. 6
      common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java
  27. 4
      common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java
  28. 2
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  29. 2
      common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java
  30. 30
      common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java
  31. 11
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java
  32. 120
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java
  33. 122
      common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java
  34. 91
      common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java
  35. 88
      common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java
  36. 4
      dao/pom.xml
  37. 20
      dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java
  38. 98
      dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java
  39. 2
      monitoring/pom.xml
  40. 133
      monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java
  41. 41
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  42. 8
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
  43. 21
      netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategy.java
  44. 82
      netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java
  45. 95
      netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java
  46. 45
      pom.xml
  47. 17
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java
  48. 3
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java
  49. 76
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java
  50. 4
      tools/pom.xml
  51. 6
      tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java
  52. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html
  53. 4
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  54. 8
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  55. 3
      ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html
  56. 2
      ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html
  57. 3
      ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html
  58. 3
      ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html
  59. 2
      ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html
  60. 2
      ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html
  61. 2
      ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html
  62. 2
      ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html
  63. 2
      ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html
  64. 4
      ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html
  65. 4
      ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html
  66. 2
      ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html
  67. 2
      ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html
  68. 6
      ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html
  69. 2
      ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html
  70. 9
      ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts
  71. 168
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  72. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html
  73. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts
  74. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html
  75. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html
  76. 53
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.scss
  77. 10
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts
  78. 64
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts
  79. 13
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
  80. 1
      ui-ngx/src/app/shared/models/constants.ts
  81. 3
      ui-ngx/src/app/shared/models/dynamic-form.models.ts
  82. 14
      ui-ngx/src/assets/locale/locale.constant-en_US.json

63
application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Bottom right elbow connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 L 125,100 Q 100,100 100,125 L 100, 200';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,8 +53,8 @@
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
@ -130,36 +130,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -169,7 +146,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -177,11 +154,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -190,8 +167,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -199,13 +176,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -214,13 +192,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -228,7 +207,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -250,5 +229,5 @@
}
]
}]]></tb:metadata>
<path d="M200 100H132C115 100 100 115 100 132V200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
<path d="M 100,200 L 100,125 Q 100,100 125,100 L 200, 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

69
application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Bottom tee connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\nconst bottomLine = \"M 100,200 V 103\";\nconst bottomLineReversed = \"M 100,103 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -367,36 +367,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -406,7 +383,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -414,11 +391,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -427,8 +404,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -436,13 +413,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -451,13 +429,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -465,7 +444,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

74
application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Cross connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst topLine = \"M100 97L100 0\";\nconst topLineReversed = \"M 100,0 V 97\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\nconst bottomLine = \"M 100,200 V 103\";\nconst bottomLineReversed = \"M 100,103 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -406,8 +406,8 @@
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -483,14 +483,6 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
@ -500,29 +492,15 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
"disabled": false
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -530,11 +508,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -543,8 +521,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -552,13 +530,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -567,13 +546,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -581,7 +561,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 19 KiB

112
application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg

@ -3,7 +3,7 @@
"description": "Horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 H 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n\n",
"tags": [
{
"tag": "arrow",
@ -12,7 +12,7 @@
},
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,18 +53,18 @@
"defaultWidgetActionSettings": null
},
{
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -72,34 +72,38 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -107,20 +111,16 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -166,36 +166,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -205,7 +182,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -213,11 +190,10 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -226,20 +202,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -248,13 +227,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -262,7 +242,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

75
application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Left bottom elbow connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,200 L 100,125 Q 100,100 75,100 L 0, 100';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -32,8 +32,8 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": "SHARED_SCOPE",
"key": "flow"
},
"getTimeSeries": {
"key": "state"
@ -44,8 +44,8 @@
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -53,8 +53,8 @@
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
@ -130,36 +130,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -169,7 +146,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -177,11 +154,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -190,20 +167,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -212,13 +192,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -226,7 +207,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -248,5 +229,5 @@
}
]
}]]></tb:metadata>
<path d="M100 200L100 131C100 113.879 86.1208 100 69 100L0 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
<path d="M 0,100 L 75,100 Q 100,100 100,125 L 100, 200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

71
application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Left tee connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst topLine = \"M100 100L100 0\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst leftLineReversed = \"M 97,100 H 0\";\nconst topLine = \"M100 100L100 0\";\nconst topLineReversed = \"M 100,0 V 100\";\nconst bottomLine = \"M 100,200 V 100\";\nconst bottomLineReversed = \"M 100,100 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -367,36 +367,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -406,7 +383,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -414,11 +391,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -427,8 +404,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -436,13 +413,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -451,13 +429,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -465,7 +444,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -487,5 +466,5 @@
}
]
}]]></tb:metadata>
<path d="M100 0V200" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M100 113V87C100 87 100 100 86 100C100 100 100 113 100 113Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"/><g tb:tag="bottomLine"/></g>
<path d="M100 0V200" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M100 113V87C100 87 100 100 86 100C100 100 100 113 100 113Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"> </g><g tb:tag="bottomLine"> </g></g>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

63
application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Left top elbow connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 L 100,75 Q 100,100 75,100 L 0, 100';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,8 +53,8 @@
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
@ -130,36 +130,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -169,7 +146,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -177,11 +154,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -190,8 +167,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -199,13 +176,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -214,13 +192,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -228,7 +207,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -250,5 +229,5 @@
}
]
}]]></tb:metadata>
<path d="M0 100H69C86.1208 100 100 86.1208 100 69V0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
<path d="M 0,100 L 75,100 Q 100,100 100,75 L 100, 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

118
application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg

@ -1,10 +1,9 @@
<svg width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg">
<tb:metadata><![CDATA[{
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Long horizontal connector",
"description": "Long horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 2,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 400,100 H 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "arrow",
@ -13,7 +12,7 @@
},
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -54,18 +53,18 @@
"defaultWidgetActionSettings": null
},
{
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -73,34 +72,38 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -108,20 +111,16 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -167,36 +166,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -206,7 +182,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -214,11 +190,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -227,20 +203,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -249,13 +228,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -263,7 +243,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -285,5 +265,5 @@
}
]
}]]></tb:metadata>
<path d="m0 100h400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m229 100-58 29v-58z" fill="#1a1a1a" tb:tag="arrow"/><g tb:tag="animationGroup"/>
<path d="M0 100H400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m229 100-58 29v-58z" fill="#1a1a1a" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

119
application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg

@ -3,7 +3,7 @@
"description": "Long vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 2,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 V 400';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n\n",
"tags": [
{
"tag": "arrow",
@ -12,7 +12,7 @@
},
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,18 +53,18 @@
"defaultWidgetActionSettings": null
},
{
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -72,34 +72,38 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -107,20 +111,16 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -166,36 +166,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -205,7 +182,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -213,11 +190,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -226,20 +203,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -248,21 +228,20 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
"min": 0,
"step": 1
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -284,5 +263,5 @@
}
]
}]]></tb:metadata>
<path d="m100 400v-400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m100 171 29 58h-58l29-58z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
<path d="M 100,400 V 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m100 171 29 58h-58l29-58z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

69
application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Right tee connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst rightLine = \"M103 100H200\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst topLineReversed = \"M 100,0 V 100\";\nconst rightLine = \"M103 100H200\";\nconst rightLineReversed = \"M 200,100 H 103\";\nconst bottomLine = \"M 100,200 V 100\";\nconst bottomLineReversed = \"M 100,100 V 200\";\n\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -367,36 +367,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -406,7 +383,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -414,11 +391,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -427,8 +404,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -436,13 +413,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -451,13 +429,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -465,7 +444,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

63
application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Top right elbow connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 L 125,100 Q 100,100 100,75 L 100, 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,8 +53,8 @@
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
@ -130,36 +130,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -169,7 +146,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -177,11 +154,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -190,8 +167,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -199,13 +176,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -214,13 +192,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -228,7 +207,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -250,5 +229,5 @@
}
]
}]]></tb:metadata>
<path d="M100 0V69C100 86.1208 113.879 100 131 100H200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
<path d="M 100,0 L 100,75 Q 100,100 125,100 L 200, 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

69
application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Top tee connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst topLine = \"M100 97L100 0\";\nconst topLineReversed = \"M 100,0 V 97\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -367,36 +367,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -406,7 +383,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -414,11 +391,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -427,8 +404,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -436,13 +413,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -451,13 +429,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -465,7 +444,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

115
application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg

@ -3,7 +3,7 @@
"description": "Vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 V 200';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "arrow",
@ -12,7 +12,7 @@
},
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,18 +53,18 @@
"defaultWidgetActionSettings": null
},
{
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -72,34 +72,38 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -107,20 +111,16 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -166,36 +166,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -205,7 +182,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -213,11 +190,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -226,20 +203,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -248,13 +228,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -262,7 +243,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -284,5 +265,5 @@
}
]
}]]></tb:metadata>
<path d="M100 200L100 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="M100 71L129 129H71L100 71Z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
<path d="M 100,200 V 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="M100 71L129 129H71L100 71Z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

2
application/src/main/data/json/system/widget_bundles/high_performance_scada_energy_system.json

@ -15,7 +15,7 @@
"hp_wind_turbine_cluster",
"hp_fuel_generator",
"hp_industrial_fuel_generator",
"hp_circuit_breaker2",
"hp_circuit_breaker",
"hp_horizontal_circuit_breaker",
"hp_voltage_relay",
"hp_3_phase_voltage_relay",

7
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -47,6 +47,7 @@ import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.AuthExceptionHandler;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider;
@ -129,6 +130,9 @@ public class ThingsboardSecurityConfiguration {
@Autowired
private RateLimitProcessingFilter rateLimitProcessingFilter;
@Autowired
private AuthExceptionHandler authExceptionHandler;
@Bean
protected PayloadSizeFilter payloadSizeFilter() {
return new PayloadSizeFilter(maxPayloadSizeConfig);
@ -235,7 +239,8 @@ public class ThingsboardSecurityConfiguration {
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(payloadSizeFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(authExceptionHandler, buildRestLoginProcessingFilter().getClass());
if (oauth2Configuration != null) {
http.oauth2Login(login -> login
.authorizationEndpoint(config -> config

9
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -437,14 +437,7 @@ public abstract class BaseController {
} else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof DataAccessException) {
if (!logControllerErrorStackTrace) { // not to log the error twice
log.warn("Database error: {} - {}", exception.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(exception));
}
if (cause instanceof ConstraintViolationException) {
return new ThingsboardException(ExceptionUtils.getRootCause(exception).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
}
return new ThingsboardException(exception, ThingsboardErrorCode.DATABASE);
} else if (exception instanceof EntityVersionMismatchException) {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT);
}

5
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -270,7 +270,10 @@ public class CalculatedFieldController extends BaseController {
for (EntityId referencedEntityId : referencedEntityIds) {
EntityType entityType = referencedEntityId.getEntityType();
switch (entityType) {
case TENANT, CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
case TENANT -> {
return;
}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default ->
throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
}

26
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java

@ -21,7 +21,9 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
@ -139,6 +141,8 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
ThingsboardException thingsboardException = (ThingsboardException) exception;
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.SUBSCRIPTION_VIOLATION) {
handleSubscriptionException((ThingsboardException) exception, response);
} else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) {
handleDatabaseException(thingsboardException.getCause(), response);
} else {
handleThingsboardException((ThingsboardException) exception, response);
}
@ -148,8 +152,10 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
handleAccessDeniedException(response);
} else if (exception instanceof AuthenticationException) {
handleAuthenticationException((AuthenticationException) exception, response);
} else if (exception instanceof MaxPayloadSizeExceededException) {
} else if (exception instanceof MaxPayloadSizeExceededException) {
handleMaxPayloadSizeExceededException(response, (MaxPayloadSizeExceededException) exception);
} else if (exception instanceof DataAccessException e) {
handleDatabaseException(e, response);
} else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(),
@ -201,6 +207,17 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
JacksonUtil.fromBytes(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class));
}
private void handleDatabaseException(Throwable databaseException, HttpServletResponse response) throws IOException {
ThingsboardErrorResponse errorResponse;
if (databaseException instanceof ConstraintViolationException) {
errorResponse = ThingsboardErrorResponse.of(ExceptionUtils.getRootCause(databaseException).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST);
} else {
log.warn("Database error: {} - {}", databaseException.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(databaseException));
errorResponse = ThingsboardErrorResponse.of("Database error", ThingsboardErrorCode.DATABASE, HttpStatus.INTERNAL_SERVER_ERROR);
}
writeResponse(errorResponse, response);
}
private void handleAccessDeniedException(HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.FORBIDDEN.value());
JacksonUtil.writeValue(response.getWriter(),
@ -233,4 +250,11 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
}
}
// TODO: refactor this class to use this method instead of boilerplate JacksonUtil.writeValue(response.getWriter(), ...
private void writeResponse(ThingsboardErrorResponse errorResponse, HttpServletResponse response) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(errorResponse.getStatus());
JacksonUtil.writeValue(response.getWriter(), errorResponse);
}
}

5
application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.edqs;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.edqs.EdqsConfig;
import org.thingsboard.server.queue.kafka.TbKafkaAdmin;
import org.thingsboard.server.queue.kafka.TbKafkaSettings;
@ -32,11 +33,11 @@ public class KafkaEdqsSyncService extends EdqsSyncService {
private final boolean syncNeeded;
public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings, EdqsConfig edqsConfig) {
public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings, TopicService topicService, EdqsConfig edqsConfig) {
TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap());
this.syncNeeded = kafkaAdmin.areAllTopicsEmpty(IntStream.range(0, edqsConfig.getPartitions())
.mapToObj(partition -> TopicPartitionInfo.builder()
.topic(edqsConfig.getEventsTopic())
.topic(topicService.buildTopicName(edqsConfig.getEventsTopic()))
.partition(partition)
.build().getFullTopicName())
.collect(Collectors.toSet()));

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -251,7 +251,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
mainConsumer.update(event.getCorePartitions());
usageStatsConsumer.subscribe(event.getCorePartitions()
.stream()
.map(tpi -> tpi.newByTopic(usageStatsConsumer.getConsumer().getTopic()))
.map(tpi -> tpi.withTopic(usageStatsConsumer.getConsumer().getTopic()))
.collect(Collectors.toSet()));
}

46
application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
@Component
@RequiredArgsConstructor
@Slf4j
public class AuthExceptionHandler extends OncePerRequestFilter {
private final ThingsboardErrorResponseHandler errorResponseHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
try {
filterChain.doFilter(request, response);
} catch (AuthenticationException e) {
throw e;
} catch (Exception e) {
errorResponseHandler.handle(e, response);
}
}
}

4
common/cluster-api/pom.xml

@ -60,10 +60,6 @@
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-schema-validator</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

4
common/dao-api/pom.xml

@ -56,10 +56,6 @@
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-schema-validator</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

3
common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java

@ -31,7 +31,8 @@ public enum ThingsboardErrorCode {
TOO_MANY_UPDATES(34),
VERSION_CONFLICT(35),
SUBSCRIPTION_VIOLATION(40),
PASSWORD_VIOLATION(45);
PASSWORD_VIOLATION(45),
DATABASE(46);
private int errorCode;

8
common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java

@ -58,6 +58,7 @@ import org.thingsboard.server.queue.TbQueueResponseTemplate;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.edqs.EdqsComponent;
import org.thingsboard.server.queue.edqs.EdqsConfig;
@ -88,6 +89,7 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
private final EdqsRepository repository;
private final EdqsConfig config;
private final EdqsPartitionService partitionService;
private final TopicService topicService;
private final ConfigurableApplicationContext applicationContext;
private final EdqsStateService stateService;
@ -123,7 +125,7 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToEdqsMsg>>create()
.queueKey(new QueueKey(ServiceType.EDQS, config.getEventsTopic()))
.topic(config.getEventsTopic())
.topic(topicService.buildTopicName(config.getEventsTopic()))
.pollInterval(config.getPollInterval())
.msgPackProcessor((msgs, consumer, config) -> {
for (TbProtoQueueMsg<ToEdqsMsg> queueMsg : msgs) {
@ -164,9 +166,9 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
try {
Set<TopicPartitionInfo> newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS));
stateService.process(withTopic(newPartitions, config.getStateTopic()));
stateService.process(withTopic(newPartitions, topicService.buildTopicName(config.getStateTopic())));
// eventsConsumer's partitions are updated by stateService
responseTemplate.subscribe(withTopic(newPartitions, config.getRequestsTopic())); // TODO: we subscribe to partitions before we are ready. implement consumer-per-partition version for request template
responseTemplate.subscribe(withTopic(newPartitions, topicService.buildTopicName(config.getRequestsTopic()))); // TODO: we subscribe to partitions before we are ready. implement consumer-per-partition version for request template
Set<TopicPartitionInfo> oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS));
if (CollectionsUtil.isNotEmpty(oldPartitions)) {

6
common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java

@ -36,6 +36,7 @@ import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.common.state.KafkaQueueStateService;
import org.thingsboard.server.queue.common.state.QueueStateService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.edqs.EdqsConfig;
import org.thingsboard.server.queue.edqs.KafkaEdqsComponent;
import org.thingsboard.server.queue.edqs.KafkaEdqsQueueFactory;
@ -59,6 +60,7 @@ public class KafkaEdqsStateService implements EdqsStateService {
private final EdqsConfig config;
private final EdqsPartitionService partitionService;
private final KafkaEdqsQueueFactory queueFactory;
private final TopicService topicService;
@Autowired
@Lazy
private EdqsProcessor edqsProcessor;
@ -78,7 +80,7 @@ public class KafkaEdqsStateService implements EdqsStateService {
TbKafkaAdmin queueAdmin = queueFactory.getEdqsQueueAdmin();
stateConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToEdqsMsg>>create()
.queueKey(new QueueKey(ServiceType.EDQS, config.getStateTopic()))
.topic(config.getStateTopic())
.topic(topicService.buildTopicName(config.getStateTopic()))
.pollInterval(config.getPollInterval())
.msgPackProcessor((msgs, consumer, config) -> {
for (TbProtoQueueMsg<ToEdqsMsg> queueMsg : msgs) {
@ -176,7 +178,7 @@ public class KafkaEdqsStateService implements EdqsStateService {
if (queueStateService.getPartitions().isEmpty()) {
Set<TopicPartitionInfo> allPartitions = IntStream.range(0, config.getPartitions())
.mapToObj(partition -> TopicPartitionInfo.builder()
.topic(config.getEventsTopic())
.topic(topicService.buildTopicName(config.getEventsTopic()))
.partition(partition)
.build())
.collect(Collectors.toSet());

4
common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java

@ -57,10 +57,6 @@ public class TopicPartitionInfo {
this(topic, tenantId, partition, false, myPartition);
}
public TopicPartitionInfo newByTopic(String topic) {
return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.useInternalPartition, this.myPartition);
}
public String getTopic() {
return topic;
}

2
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -156,7 +156,7 @@ public class HashPartitionService implements PartitionService {
@Override
public String getTopic(QueueKey queueKey) {
return partitionTopicsMap.get(queueKey);
return topicService.buildTopicName(partitionTopicsMap.get(queueKey));
}
private void doInitRuleEnginePartitions() {

2
common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java

@ -131,7 +131,7 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient {
report.forEach((parent, statsMsg) -> {
try {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, parent.getTenantId(), parent.getId())
.newByTopic(msgProducer.getDefaultTopic());
.withTopic(msgProducer.getDefaultTopic());
reportStatsPerTpi.computeIfAbsent(tpi, k -> new ArrayList<>()).add(statsMsg.build());
} catch (TenantNotFoundException e) {
log.debug("Couldn't report usage stats for non-existing tenant: {}", e.getTenantId());

30
common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java

@ -134,19 +134,29 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
}
}
@Override
public ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) {
public String validate(TenantId tenantId, String scriptBody) {
if (isExecEnabled(tenantId)) {
if (scriptBodySizeExceeded(scriptBody)) {
return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize()));
return format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize());
}
UUID scriptId = UUID.randomUUID();
requestsCounter.increment();
return withTimeoutAndStatsCallback(scriptId, null,
doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout());
} else {
return error("Script Execution is disabled due to API limits!");
return "Script Execution is disabled due to API limits!";
}
return null;
}
@Override
public ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) {
String validationError = validate(tenantId, scriptBody);
if (validationError != null) {
return error(validationError);
}
UUID scriptId = UUID.randomUUID();
requestsCounter.increment();
return withTimeoutAndStatsCallback(scriptId, null,
doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout());
}
@Override
@ -269,7 +279,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
}
}
private boolean scriptBodySizeExceeded(String scriptBody) {
public boolean scriptBodySizeExceeded(String scriptBody) {
if (getMaxScriptBodySize() <= 0) return false;
return scriptBody.length() > getMaxScriptBodySize();
}
@ -297,7 +307,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
return result != null && result.length() > getMaxResultSize();
}
private <T> ListenableFuture<T> error(String message) {
public <T> ListenableFuture<T> error(String message) {
return Futures.immediateFailedFuture(new RuntimeException(message));
}

11
common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java

@ -35,6 +35,8 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import static java.lang.String.format;
/**
* Created by ashvayka on 26.09.18.
*/
@ -93,6 +95,15 @@ public abstract class AbstractJsInvokeService extends AbstractScriptInvokeServic
doRelease(scriptId, scriptInfoMap.remove(scriptId));
}
@Override
public String validate(TenantId tenantId, String scriptBody) {
String errorMessage = super.validate(tenantId, scriptBody);
if (errorMessage == null) {
return JsValidator.validate(scriptBody);
}
return errorMessage;
}
protected abstract ListenableFuture<UUID> doEval(UUID scriptId, JsScriptInfo jsInfo, String scriptBody);
protected abstract ListenableFuture<Object> doInvokeFunction(UUID scriptId, JsScriptInfo jsInfo, Object[] args);

120
common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java

@ -0,0 +1,120 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.js;
import java.util.regex.Pattern;
public class JsValidator {
static final Pattern ASYNC_PATTERN = Pattern.compile("\\basync\\b");
static final Pattern AWAIT_PATTERN = Pattern.compile("\\bawait\\b");
static final Pattern PROMISE_PATTERN = Pattern.compile("\\bPromise\\b");
static final Pattern SET_TIMEOUT_PATTERN = Pattern.compile("\\bsetTimeout\\b");
public static String validate(String scriptBody) {
if (scriptBody == null || scriptBody.trim().isEmpty()) {
return "Script body is empty";
}
//Quick check
if (!ASYNC_PATTERN.matcher(scriptBody).find()
&& !AWAIT_PATTERN.matcher(scriptBody).find()
&& !PROMISE_PATTERN.matcher(scriptBody).find()
&& !SET_TIMEOUT_PATTERN.matcher(scriptBody).find()) {
return null;
}
//Recheck if quick check failed. Ignoring comments and strings
String[] lines = scriptBody.split("\\r?\\n");
boolean insideMultilineComment = false;
for (String line : lines) {
String stripped = line;
// Handle multiline comments
if (insideMultilineComment) {
if (line.contains("*/")) {
insideMultilineComment = false;
stripped = line.substring(line.indexOf("*/") + 2); // continue after comment
} else {
continue; // skip line inside multiline comment
}
}
// Check for start of multiline comment
if (stripped.contains("/*")) {
int start = stripped.indexOf("/*");
int end = stripped.indexOf("*/", start + 2);
if (end != -1) {
// Inline multiline comment
stripped = stripped.substring(0, start) + stripped.substring(end + 2);
} else {
// Starts a block comment, continues on next lines
insideMultilineComment = true;
stripped = stripped.substring(0, start);
}
}
stripped = stripInlineComment(stripped);
stripped = stripStringLiterals(stripped);
if (ASYNC_PATTERN.matcher(stripped).find()) {
return "Script must not contain 'async' keyword.";
}
if (AWAIT_PATTERN.matcher(stripped).find()) {
return "Script must not contain 'await' keyword.";
}
if (PROMISE_PATTERN.matcher(stripped).find()) {
return "Script must not use 'Promise'.";
}
if (SET_TIMEOUT_PATTERN.matcher(stripped).find()) {
return "Script must not use 'setTimeout' method.";
}
}
return null;
}
private static String stripInlineComment(String line) {
int index = line.indexOf("//");
return index >= 0 ? line.substring(0, index) : line;
}
private static String stripStringLiterals(String line) {
StringBuilder sb = new StringBuilder();
boolean inSingleQuote = false;
boolean inDoubleQuote = false;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
continue;
} else if (c == '\'' && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
continue;
}
if (!inSingleQuote && !inDoubleQuote) {
sb.append(c);
}
}
return sb.toString();
}
}

122
common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java

@ -0,0 +1,122 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.StatsCounter;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
class AbstractScriptInvokeServiceTest {
AbstractScriptInvokeService service;
final UUID id = UUID.randomUUID();
final String scriptBody = "return true;";
final TenantId tenantId = TenantId.fromUUID(UUID.fromString("2ed9a658-45a5-4812-b212-9931f5749f30"));
@BeforeEach
void setUp() {
service = mock(AbstractScriptInvokeService.class, Mockito.RETURNS_DEEP_STUBS);
// Make sure core checks always pass
doReturn(true).when(service).isExecEnabled(any());
doReturn(50000L).when(service).getMaxScriptBodySize();
// Use real implementations
doCallRealMethod().when(service).scriptBodySizeExceeded(anyString());
doCallRealMethod().when(service).eval(any(), any(), any(), any(String[].class));
doCallRealMethod().when(service).error(anyString());
doCallRealMethod().when(service).validate(any(), anyString());
}
@Test
void evalWithValidationCallTest() throws ExecutionException, InterruptedException, TimeoutException {
ReflectionTestUtils.setField(service, "requestsCounter", mock(StatsCounter.class));
ReflectionTestUtils.setField(service, "evalCallback", mock(FutureCallback.class));
doReturn(Futures.immediateFuture(id)).when(service).doEvalScript(any(), any(), anyString(), any(), any(String[].class));
var future = service.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, scriptBody, "x", "y");
assertThat(future.get(30, TimeUnit.SECONDS)).isEqualTo(id);
verify(service).validate(any(), anyString());
verify(service).validate(tenantId, scriptBody);
verify(service, never()).error(anyString());
}
@Test
void evalWithValidationCallErrorTest() throws ExecutionException, InterruptedException, TimeoutException {
doReturn(false).when(service).isExecEnabled(any());
var future = service.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, scriptBody, "x", "y");
ExecutionException ex = assertThrows(ExecutionException.class, future::get);
assertThat(ex.getCause().getMessage()).isEqualTo("Script Execution is disabled due to API limits!");
assertThat(ex.getCause()).isInstanceOf(RuntimeException.class);
verify(service).validate(any(), anyString());
verify(service).validate(tenantId, scriptBody);
verify(service).error(anyString());
}
@Test
void validateScriptBodyTestExecEnabledTest() {
assertNull(service.validate(tenantId, scriptBody));
verify(service).isExecEnabled(tenantId);
}
@Test
void validateScriptBodyTestExecDisabledTest() {
doReturn(false).when(service).isExecEnabled(tenantId);
assertThat(service.validate(tenantId, scriptBody)).isEqualTo("Script Execution is disabled due to API limits!");
verify(service).isExecEnabled(tenantId);
}
@Test
void validateScriptBodySizeOKTest() {
assertNull(service.validate(tenantId, scriptBody));
verify(service).isExecEnabled(tenantId);
verify(service).scriptBodySizeExceeded(scriptBody);
}
@Test
void validateScriptBodySizeExceededTest() {
doReturn(10L).when(service).getMaxScriptBodySize();
assertThat(service.validate(tenantId, scriptBody)).isEqualTo("Script body exceeds maximum allowed size of 10 symbols");
verify(service).isExecEnabled(tenantId);
verify(service).scriptBodySizeExceeded(scriptBody);
}
}

91
common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java

@ -0,0 +1,91 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.js;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import org.thingsboard.server.common.stats.StatsCounter;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@Slf4j
class AbstractJsInvokeServiceTest {
AbstractJsInvokeService service;
final UUID id = UUID.randomUUID();
@BeforeEach
void setUp() {
service = mock(AbstractJsInvokeService.class, Mockito.RETURNS_DEEP_STUBS);
ReflectionTestUtils.setField(service, "requestsCounter", mock(StatsCounter.class));
ReflectionTestUtils.setField(service, "evalCallback", mock(FutureCallback.class));
// Make sure core checks always pass
doReturn(true).when(service).isExecEnabled(any());
doReturn(false).when(service).scriptBodySizeExceeded(anyString());
doReturn(Futures.immediateFuture(id)).when(service).doEvalScript(any(), any(), anyString(), any(), any(String[].class));
// Use real implementations
doCallRealMethod().when(service).eval(any(), any(), any(), any(String[].class));
doCallRealMethod().when(service).error(anyString());
doCallRealMethod().when(service).validate(any(), anyString());
}
@Test
void shouldReturnValidationErrorFromJsValidator() throws ExecutionException, InterruptedException {
String scriptWithAsync = "async function test() {}";
var future = service.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptWithAsync, "a", "b");
ExecutionException ex = assertThrows(ExecutionException.class, future::get);
assertTrue(ex.getCause().getMessage().contains("Script must not contain 'async' keyword."));
assertThat(ex.getCause()).isInstanceOf(RuntimeException.class);
verify(service).isExecEnabled(any());
verify(service).scriptBodySizeExceeded(any());
}
@Test
void shouldPassValidationAndCallSuperEval() throws ExecutionException, InterruptedException, TimeoutException {
String validScript = "function test() { return 42; }";
var result = service.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, validScript, "x", "y");
assertThat(result.get(30, TimeUnit.SECONDS)).isEqualTo(id);
verify(service, times(1)).isExecEnabled(any());
verify(service, times(1)).scriptBodySizeExceeded(any());
}
}

88
common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java

@ -0,0 +1,88 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.js;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
class JsValidatorTest {
@ParameterizedTest(name = "should return error for script \"{0}\"")
@ValueSource(strings = {
"async function test() {}",
"const result = await someFunc();",
"const result =\nawait\tsomeFunc();",
"setTimeout(1000);",
"new Promise((resolve) => {});",
"function test() { return 42; } \n\t await test()",
"""
function init() {
await doSomething();
}
""",
})
void shouldReturnErrorForInvalidScripts(String script) {
assertNotNull(JsValidator.validate(script));
}
@ParameterizedTest(name = "should pass validation for script: \"{0}\"")
@ValueSource(strings = {
"function test() { return 42; }",
"const result = 10 * 2;",
"// async is a keyword but not used: 'const word = \"async\";'",
"let note = 'setTimeout tight';",
"const word = \"async\";",
"const word = \"setTimeout\";",
"const word = \"Promise\";",
"const word = \"await\";",
"const word = 'async';",
"const word = 'setTimeout';",
"const word = 'Promise';",
"const word = 'await';",
"//function test() { return 42; }",
"// const result = 10 * 2;",
"// async is a keyword but not used: 'const word = \"async\";'",
"//setTimeout(1);",
"a=b+c; // await for a day",
"return new // Promise((resolve) => {",
"hello(); // async is a keyword but not used: 'const word = \"async\";'",
"setGoal(a); //setTimeout(1);",
" /* new Promise((resolve) => {}); // */ return 'await';",
" /* async */ function calc() {",
"/* async function abc() { \n await new Promise ( \t setTimeout () ) \n } \n*/",
})
void shouldReturnNullForValidScripts(String script) {
assertNull(JsValidator.validate(script));
}
@ParameterizedTest(name = "should return 'Script body is empty' for input: \"{0}\"")
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void shouldReturnErrorForEmptyOrNullScripts(String script) {
assertEquals("Script body is empty", JsValidator.validate(script));
}
}

4
dao/pom.xml

@ -59,6 +59,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>util</artifactId>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

20
dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java

@ -16,10 +16,10 @@
package org.thingsboard.server.dao.component;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.main.JsonSchemaFactory;
import com.github.fge.jsonschema.main.JsonValidator;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -36,6 +36,7 @@ import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.Validator;
import java.util.Optional;
import java.util.Set;
/**
* @author Andrew Shvayka
@ -89,15 +90,18 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic
@Override
public boolean validate(TenantId tenantId, ComponentDescriptor component, JsonNode configuration) {
JsonValidator validator = JsonSchemaFactory.byDefault().getValidator();
try {
if (!component.getConfigurationDescriptor().has("schema")) {
throw new DataValidationException("Configuration descriptor doesn't contain schema property!");
}
JsonNode configurationSchema = component.getConfigurationDescriptor().get("schema");
ProcessingReport report = validator.validate(configurationSchema, configuration);
return report.isSuccess();
} catch (ProcessingException e) {
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4);
JsonSchema schema = factory.getSchema(configurationSchema);
Set<ValidationMessage> validationMessages = schema.validate(configuration);
return validationMessages.isEmpty();
} catch (Exception e) {
throw new IncorrectParameterException(e.getMessage(), e);
}
}

98
dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java

@ -0,0 +1,98 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.component;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentScope;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class BaseComponentDescriptorServiceTest {
private BaseComponentDescriptorService service;
private ComponentDescriptor componentDescriptor;
private TenantId tenantId;
@BeforeEach
void setUp() {
service = Mockito.spy(BaseComponentDescriptorService.class);
tenantId = TenantId.SYS_TENANT_ID;
// Create a simple component descriptor
componentDescriptor = new ComponentDescriptor();
componentDescriptor.setType(ComponentType.ACTION);
componentDescriptor.setScope(ComponentScope.TENANT);
componentDescriptor.setClusteringMode(ComponentClusteringMode.ENABLED);
componentDescriptor.setName("Test Component");
componentDescriptor.setClazz("org.thingsboard.test.TestComponent");
// Create configuration descriptor with schema from JSON string
String configDescriptorJson = """
{
"schema": {
"type": "object",
"properties": {
"testField": {
"type": "string"
}
},
"required": ["testField"]
}
}""";
componentDescriptor.setConfigurationDescriptor(JacksonUtil.toJsonNode(configDescriptorJson));
}
@Test
void testValidate() {
// Create valid configuration from JSON string
String validConfigJson = "{\"testField\": \"test value\"}";
JsonNode validConfig = JacksonUtil.toJsonNode(validConfigJson);
// Create invalid configuration (missing required field) from JSON string
String invalidConfigJson = "{}";
JsonNode invalidConfig = JacksonUtil.toJsonNode(invalidConfigJson);
// Test valid configuration
boolean validResult = service.validate(tenantId, componentDescriptor, validConfig);
assertTrue(validResult, "Valid configuration should pass validation");
// Test invalid configuration
boolean invalidResult = service.validate(tenantId, componentDescriptor, invalidConfig);
assertFalse(invalidResult, "Invalid configuration should fail validation");
// Test with component descriptor without schema
ComponentDescriptor noSchemaDescriptor = new ComponentDescriptor(componentDescriptor);
noSchemaDescriptor.setConfigurationDescriptor(JacksonUtil.toJsonNode("{}"));
// Should throw exception when schema is missing
assertThrows(IncorrectParameterException.class, () -> {
service.validate(tenantId, noSchemaDescriptor, validConfig);
}, "Should throw exception when schema is missing");
}
}

2
monitoring/pom.xml

@ -42,8 +42,6 @@
<pkg.implementationTitle>ThingsBoard Monitoring Service</pkg.implementationTitle>
<pkg.mainClass>org.thingsboard.monitoring.ThingsboardMonitoringApplication</pkg.mainClass>
<californium.version>2.6.1</californium.version>
<leshan.version>2.0.0-M4</leshan.version>
</properties>
<dependencies>

133
monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java

@ -20,13 +20,16 @@ import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.californium.core.observe.ObservationStore;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.leshan.client.californium.LeshanClient;
import org.eclipse.leshan.client.californium.LeshanClientBuilder;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.scandium.config.DtlsConfig;
import org.eclipse.leshan.client.LeshanClient;
import org.eclipse.leshan.client.LeshanClientBuilder;
import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointsProvider;
import org.eclipse.leshan.client.californium.endpoint.ClientProtocolProvider;
import org.eclipse.leshan.client.californium.endpoint.coap.CoapOscoreProtocolProvider;
import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientProtocolProvider;
import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider;
import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.client.object.Server;
@ -34,9 +37,8 @@ import org.eclipse.leshan.client.observer.LwM2mClientObserver;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
import org.eclipse.leshan.client.resource.ObjectsInitializer;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.californium.EndpointFactory;
import org.eclipse.leshan.core.model.InvalidDDFFileException;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectLoader;
@ -53,7 +55,6 @@ import org.thingsboard.monitoring.util.ResourceUtils;
import javax.security.auth.Destroyable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -95,9 +96,11 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
}
Security security = noSec(serverUri, 123);
NetworkConfig coapConfig = new NetworkConfig().setString(NetworkConfig.Keys.COAP_PORT, StringUtils.substringAfterLast(serverUri, ":"));
LeshanClient leshanClient;
Configuration coapConfig = new Configuration();
String portStr = StringUtils.substringAfterLast(serverUri, ":");
if (StringUtils.isNotEmpty(portStr)) {
coapConfig.set(CoapConfig.COAP_PORT, Integer.parseInt(portStr));
}
LwM2mModel model = new StaticModel(models);
ObjectsInitializer initializer = new ObjectsInitializer(model);
@ -105,118 +108,121 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(5)));
initializer.setInstancesForObject(DEVICE, this);
initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder();
dtlsConfig.setRecommendedCipherSuitesOnly(true);
dtlsConfig.setClientOnly();
DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory();
engineFactory.setReconnectOnUpdate(false);
engineFactory.setResumeOnConnect(true);
// Create client endpoints Provider
List<ClientProtocolProvider> protocolProvider = new ArrayList<>();
protocolProvider.add(new CoapOscoreProtocolProvider());
protocolProvider.add(new CoapsClientProtocolProvider());
CaliforniumClientEndpointsProvider.Builder endpointsBuilder = new CaliforniumClientEndpointsProvider.Builder(
protocolProvider.toArray(new ClientProtocolProvider[protocolProvider.size()]));
EndpointFactory endpointFactory = new EndpointFactory() {
// Create Californium Configuration
Configuration clientCoapConfig = endpointsBuilder.createDefaultConfiguration();
@Override
public CoapEndpoint createUnsecuredEndpoint(InetSocketAddress address, NetworkConfig coapConfig,
ObservationStore store) {
CoapEndpoint.Builder builder = new CoapEndpoint.Builder();
builder.setInetSocketAddress(address);
builder.setNetworkConfig(coapConfig);
return builder.build();
}
// Set some DTLS stuff
clientCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY);
clientCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, true);
@Override
public CoapEndpoint createSecuredEndpoint(DtlsConnectorConfig dtlsConfig, NetworkConfig coapConfig,
ObservationStore store) {
CoapEndpoint.Builder builder = new CoapEndpoint.Builder();
DtlsConnectorConfig.Builder dtlsConfigBuilder = new DtlsConnectorConfig.Builder(dtlsConfig);
builder.setConnector(new DTLSConnector(dtlsConfigBuilder.build()));
builder.setNetworkConfig(coapConfig);
return builder.build();
}
};
// Set Californium Configuration
endpointsBuilder.setConfiguration(clientCoapConfig);
// creates EndpointsProvider
List<LwM2mClientEndpointsProvider> endpointsProvider = new ArrayList<>();
endpointsProvider.add(endpointsBuilder.build());
// Configure registration engine
DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory();
engineFactory.setReconnectOnUpdate(false);
engineFactory.setResumeOnConnect(true);
// Build the client
LeshanClientBuilder builder = new LeshanClientBuilder(endpoint);
builder.setObjects(initializer.createAll());
builder.setCoapConfig(coapConfig);
builder.setDtlsConfig(dtlsConfig);
builder.setEndpointsProviders(endpointsProvider.toArray(new LwM2mClientEndpointsProvider[endpointsProvider.size()]));
builder.setRegistrationEngineFactory(engineFactory);
builder.setEndpointFactory(endpointFactory);
builder.setDecoder(new DefaultLwM2mDecoder(false));
builder.setEncoder(new DefaultLwM2mEncoder(false));
leshanClient = builder.build();
// Add observer
LwM2mClientObserver observer = new LwM2mClientObserver() {
@Override
public void onBootstrapStarted(ServerIdentity bsserver, BootstrapRequest request) {}
public void onBootstrapStarted(LwM2mServer bsserver, BootstrapRequest request) {
// No implementation needed
}
@Override
public void onBootstrapSuccess(ServerIdentity bsserver, BootstrapRequest request) {}
public void onBootstrapSuccess(LwM2mServer bsserver, BootstrapRequest request) {
// No implementation needed
}
@Override
public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request,
ResponseCode responseCode, String errorMessage, Exception cause) {}
public void onBootstrapFailure(LwM2mServer bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
// No implementation needed
}
@Override
public void onBootstrapTimeout(ServerIdentity bsserver, BootstrapRequest request) {}
public void onBootstrapTimeout(LwM2mServer bsserver, BootstrapRequest request) {
// No implementation needed
}
@Override
public void onRegistrationStarted(ServerIdentity server, RegisterRequest request) {
public void onRegistrationStarted(LwM2mServer server, RegisterRequest request) {
log.debug("onRegistrationStarted [{}]", request.getEndpointName());
}
@Override
public void onRegistrationSuccess(ServerIdentity server, RegisterRequest request, String registrationID) {
public void onRegistrationSuccess(LwM2mServer server, RegisterRequest request, String registrationID) {
log.debug("onRegistrationSuccess [{}] [{}]", request.getEndpointName(), registrationID);
}
@Override
public void onRegistrationFailure(ServerIdentity server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
public void onRegistrationFailure(LwM2mServer server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
log.debug("onRegistrationFailure [{}] [{}] [{}]", request.getEndpointName(), responseCode, errorMessage);
}
@Override
public void onRegistrationTimeout(ServerIdentity server, RegisterRequest request) {
public void onRegistrationTimeout(LwM2mServer server, RegisterRequest request) {
log.debug("onRegistrationTimeout [{}]", request.getEndpointName());
}
@Override
public void onUpdateStarted(ServerIdentity server, UpdateRequest request) {
public void onUpdateStarted(LwM2mServer server, UpdateRequest request) {
log.debug("onUpdateStarted [{}]", request.getRegistrationId());
}
@Override
public void onUpdateSuccess(ServerIdentity server, UpdateRequest request) {
public void onUpdateSuccess(LwM2mServer server, UpdateRequest request) {
log.debug("onUpdateSuccess [{}]", request.getRegistrationId());
}
@Override
public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
public void onUpdateFailure(LwM2mServer server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
log.debug("onUpdateFailure [{}]", request.getRegistrationId());
}
@Override
public void onUpdateTimeout(ServerIdentity server, UpdateRequest request) {
public void onUpdateTimeout(LwM2mServer server, UpdateRequest request) {
log.debug("onUpdateTimeout [{}]", request.getRegistrationId());
}
@Override
public void onDeregistrationStarted(ServerIdentity server, DeregisterRequest request) {
public void onDeregistrationStarted(LwM2mServer server, DeregisterRequest request) {
log.debug("onDeregistrationStarted [{}]", request.getRegistrationId());
}
@Override
public void onDeregistrationSuccess(ServerIdentity server, DeregisterRequest request) {
log.debug("onDeregistrationStarted [{}]", request.getRegistrationId());
public void onDeregistrationSuccess(LwM2mServer server, DeregisterRequest request) {
log.debug("onDeregistrationSuccess [{}]", request.getRegistrationId());
}
@Override
public void onDeregistrationFailure(ServerIdentity server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
public void onDeregistrationFailure(LwM2mServer server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
log.debug("onDeregistrationFailure [{}] [{}] [{}]", request.getRegistrationId(), responseCode, errorMessage);
}
@Override
public void onDeregistrationTimeout(ServerIdentity server, DeregisterRequest request) {
public void onDeregistrationTimeout(LwM2mServer server, DeregisterRequest request) {
log.debug("onDeregistrationTimeout [{}]", request.getRegistrationId());
}
@ -224,7 +230,6 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
public void onUnexpectedError(Throwable unexpectedError) {
log.debug("onUnexpectedError [{}]", unexpectedError.toString());
}
};
leshanClient.addObserver(observer);
@ -239,17 +244,17 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
}
@Override
public ReadResponse read(ServerIdentity identity, int resourceId) {
public ReadResponse read(LwM2mServer server, int resourceId) {
if (supportedResources.contains(resourceId)) {
return ReadResponse.success(resourceId, data);
}
return super.read(identity, resourceId);
return super.read(server, resourceId);
}
@SneakyThrows
public void send(String data, int resource) {
this.data = data;
fireResourcesChange(resource);
fireResourceChange(resource);
}
@Override

41
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java

@ -29,7 +29,6 @@ import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -46,7 +45,6 @@ public class ContainerTestSuite {
final static boolean IS_REDIS_SENTINEL = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisSentinel"));
final static boolean IS_REDIS_SSL = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisSsl"));
final static boolean IS_HYBRID_MODE = Boolean.parseBoolean(System.getProperty("blackBoxTests.hybridMode"));
final static String QUEUE_TYPE = System.getProperty("blackBoxTests.queue", "kafka");
private static final String SOURCE_DIR = "./../../docker/";
private static final String TB_CORE_LOG_REGEXP = ".*Starting polling for events.*";
private static final String TRANSPORTS_LOG_REGEXP = ".*Going to recalculate partitions.*";
@ -122,44 +120,13 @@ public class ContainerTestSuite {
new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")),
new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid-test-extras.yml" : "docker-compose.postgres-test-extras.yml")),
new File(targetDir + "docker-compose.postgres.volumes.yml"),
new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"),
new File(targetDir + "docker-compose.kafka.yml"),
new File(targetDir + resolveRedisComposeFile()),
new File(targetDir + resolveRedisComposeVolumesFile()),
new File(targetDir + ("docker-selenium.yml"))
));
Map<String, String> queueEnv = new HashMap<>();
queueEnv.put("TB_QUEUE_TYPE", QUEUE_TYPE);
switch (QUEUE_TYPE) {
case "kafka":
composeFiles.add(new File(targetDir + "docker-compose.kafka.yml"));
break;
case "aws-sqs":
replaceInFile(targetDir, "queue-aws-sqs.env",
Map.of("YOUR_KEY", getSysProp("blackBoxTests.awsKey"),
"YOUR_SECRET", getSysProp("blackBoxTests.awsSecret"),
"YOUR_REGION", getSysProp("blackBoxTests.awsRegion")));
break;
case "rabbitmq":
composeFiles.add(new File(targetDir + "docker-compose.rabbitmq-server.yml"));
replaceInFile(targetDir, "queue-rabbitmq.env",
Map.of("localhost", "rabbitmq"));
break;
case "service-bus":
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_NAMESPACE_NAME", getSysProp("blackBoxTests.serviceBusNamespace"),
"YOUR_SAS_KEY_NAME", getSysProp("blackBoxTests.serviceBusSASPolicy")));
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_SAS_KEY", getSysProp("blackBoxTests.serviceBusPrimaryKey")));
break;
case "pubsub":
replaceInFile(targetDir, "queue-pubsub.env",
Map.of("YOUR_PROJECT_ID", getSysProp("blackBoxTests.pubSubProjectId"),
"YOUR_SERVICE_ACCOUNT", getSysProp("blackBoxTests.pubSubServiceAccount")));
break;
default:
throw new RuntimeException("Unsupported queue type: " + QUEUE_TYPE);
}
addToFile(targetDir, "queue-kafka.env", Map.of("TB_QUEUE_PREFIX", "test"));
addToFile(targetDir, "tb-edqs.env", Map.of("TB_QUEUE_PREFIX", "test"));
if (IS_HYBRID_MODE) {
composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml"));
@ -171,7 +138,7 @@ public class ContainerTestSuite {
.withOptions("--compatibility")
.withTailChildContainers(!skipTailChildContainers)
.withEnv(installTb.getEnv())
.withEnv(queueEnv)
.withEnv("TB_QUEUE_TYPE", "kafka")
.withEnv("LOAD_BALANCER_NAME", "")
.withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.withExposedService("broker", 1883)

8
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java

@ -81,6 +81,8 @@ final class MqttClientImpl implements MqttClient {
private final MqttHandler defaultHandler;
private final ReconnectStrategy reconnectStrategy;
private EventLoopGroup eventLoop;
private volatile Channel channel;
@ -111,6 +113,7 @@ final class MqttClientImpl implements MqttClient {
this.clientConfig = clientConfig;
this.defaultHandler = defaultHandler;
this.handlerExecutor = handlerExecutor;
this.reconnectStrategy = new ReconnectStrategyExponential(getClientConfig().getReconnectDelay());
}
/**
@ -192,7 +195,10 @@ final class MqttClientImpl implements MqttClient {
if (reconnect) {
this.reconnect = true;
}
eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), clientConfig.getReconnectDelay(), TimeUnit.SECONDS);
final long nextReconnectDelay = reconnectStrategy.getNextReconnectDelay();
log.info("[{}] Scheduling reconnect in [{}] sec", channel != null ? channel.id() : "UNKNOWN", nextReconnectDelay);
eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), nextReconnectDelay, TimeUnit.SECONDS);
}
}

21
netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategy.java

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.mqtt;
@FunctionalInterface
public interface ReconnectStrategy {
long getNextReconnectDelay();
}

82
netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java

@ -0,0 +1,82 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.mqtt;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@Getter
@Slf4j
public class ReconnectStrategyExponential implements ReconnectStrategy {
public static final int DEFAULT_RECONNECT_INTERVAL_SEC = 10;
public static final int MAX_RECONNECT_INTERVAL_SEC = 60;
public static final int EXP_MAX = 8;
public static final long JITTER_MAX = 1;
private final long reconnectIntervalMinSeconds;
private final long reconnectIntervalMaxSeconds;
private long lastDisconnectNanoTime = 0; //isotonic time
private long retryCount = 0;
public ReconnectStrategyExponential(long reconnectIntervalMinSeconds) {
this.reconnectIntervalMaxSeconds = calculateIntervalMax(reconnectIntervalMinSeconds);
this.reconnectIntervalMinSeconds = calculateIntervalMin(reconnectIntervalMinSeconds);
}
long calculateIntervalMax(long reconnectIntervalMinSeconds) {
return reconnectIntervalMinSeconds > MAX_RECONNECT_INTERVAL_SEC ? reconnectIntervalMinSeconds : MAX_RECONNECT_INTERVAL_SEC;
}
long calculateIntervalMin(long reconnectIntervalMinSeconds) {
return Math.min((reconnectIntervalMinSeconds > 0 ? reconnectIntervalMinSeconds : DEFAULT_RECONNECT_INTERVAL_SEC), this.reconnectIntervalMaxSeconds);
}
@Override
synchronized public long getNextReconnectDelay() {
final long currentNanoTime = getNanoTime();
final long coolDownSpentNanos = currentNanoTime - lastDisconnectNanoTime;
lastDisconnectNanoTime = currentNanoTime;
if (isCooledDown(coolDownSpentNanos)) {
retryCount = 0;
return reconnectIntervalMinSeconds;
}
return calculateNextReconnectDelay() + calculateJitter();
}
long calculateJitter() {
return ThreadLocalRandom.current().nextInt() >= 0 ? JITTER_MAX : 0;
}
long calculateNextReconnectDelay() {
return Math.min(reconnectIntervalMaxSeconds, reconnectIntervalMinSeconds + calculateExp(retryCount++));
}
long calculateExp(long e) {
return 1L << Math.min(e, EXP_MAX);
}
boolean isCooledDown(long coolDownSpentNanos) {
return TimeUnit.NANOSECONDS.toSeconds(coolDownSpentNanos) > reconnectIntervalMaxSeconds + reconnectIntervalMinSeconds;
}
long getNanoTime() {
return System.nanoTime();
}
}

95
netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java

@ -0,0 +1,95 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.mqtt;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.willAnswer;
import static org.thingsboard.mqtt.ReconnectStrategyExponential.EXP_MAX;
import static org.thingsboard.mqtt.ReconnectStrategyExponential.JITTER_MAX;
@Slf4j
class ReconnectStrategyExponentialTest {
@Execution(ExecutionMode.SAME_THREAD) // just for convenient log reading
@ParameterizedTest
@ValueSource(ints = {1, 0, 60})
public void exponentialReconnectDelayTest(final int reconnectIntervalMinSeconds) {
final ReconnectStrategyExponential strategy = Mockito.spy(new ReconnectStrategyExponential(reconnectIntervalMinSeconds));
log.info("=== Reconnect delay test for ReconnectStrategyExponential({}) : calculated min [{}] max [{}] ===", reconnectIntervalMinSeconds, strategy.getReconnectIntervalMinSeconds(), strategy.getReconnectIntervalMaxSeconds());
final AtomicLong nanoTime = new AtomicLong(System.nanoTime());
willAnswer((x) -> nanoTime.get()).given(strategy).getNanoTime();
final LinkedBlockingDeque<Long> jittersCaptured = new LinkedBlockingDeque<>();
final LinkedBlockingDeque<Long> expCaptured = new LinkedBlockingDeque<>();
willAnswer(captureResult(jittersCaptured)).given(strategy).calculateJitter();
willAnswer(captureResult(expCaptured)).given(strategy).calculateExp(anyLong());
for (int phase = 0; phase < 3; phase++) {
log.info("== Phase {} ==", phase);
long previousDelay = 0;
for (int i = 0; i < EXP_MAX + 4; i++) {
final long nextReconnectDelay = strategy.getNextReconnectDelay();
nanoTime.addAndGet(TimeUnit.SECONDS.toNanos(nextReconnectDelay));
log.info("Retry [{}] Delay [{}] : min [{}] exp [{}] jitter [{}]", strategy.getRetryCount(), nextReconnectDelay, strategy.getReconnectIntervalMinSeconds(), expCaptured.peekLast(), jittersCaptured.peekLast());
assertThat(previousDelay).satisfiesAnyOf(
v -> assertThat(v).isLessThanOrEqualTo(nextReconnectDelay),
v -> assertThat(v).isCloseTo(nextReconnectDelay, offset(JITTER_MAX)) // Adjust tolerance as needed
);
previousDelay = nextReconnectDelay;
}
log.info("Jitters captured: {}", drainAll(jittersCaptured));
log.info("Exponents captured: {}", drainAll(expCaptured));
assertThat(previousDelay).isCloseTo(strategy.getReconnectIntervalMaxSeconds(), offset(JITTER_MAX));
final long coolDownPeriodSec = strategy.getReconnectIntervalMinSeconds() + strategy.getReconnectIntervalMaxSeconds() + 1;
log.info("Cooling down for [{}] seconds ...", coolDownPeriodSec);
nanoTime.addAndGet(TimeUnit.SECONDS.toNanos(coolDownPeriodSec));
assertThat(strategy.isCooledDown(TimeUnit.SECONDS.toNanos(coolDownPeriodSec))).as("cooled down").isTrue();
}
}
private Answer<Long> captureResult(Collection<Long> collection) {
return invocation -> {
long result = (long) invocation.callRealMethod();
collection.add(result);
return result;
};
}
private Collection<Long> drainAll(BlockingQueue<Long> jittersCaptured) {
Collection<Long> elements = new ArrayList<>();
jittersCaptured.drainTo(elements);
return elements;
}
}

45
pom.xml

@ -42,13 +42,14 @@
<jakarta.xml.bind-api.version>4.0.2</jakarta.xml.bind-api.version>
<javax.xml.bind-api.version>2.4.0-b180830.0359</javax.xml.bind-api.version>
<jaxb-runtime.version>4.0.5</jaxb-runtime.version>
<tomcat.version>10.1.39</tomcat.version> <!--Remove after update spring-boot to new version-->
<tomcat.version>10.1.40</tomcat.version> <!-- Vulnerability fix, Remove after update spring-boot to new version-->
<net.minidev.json-smart>2.5.2</net.minidev.json-smart> <!-- Vulnerability fix, CVE-2024-57699, Remove after update spring-boot 3.2.12 to a newer version-->
<spring-boot.version>3.2.12</spring-boot.version>
<spring-data.version>3.2.12</spring-data.version>
<spring-data-redis.version>3.2.12</spring-data-redis.version>
<spring.version>6.1.15</spring.version>
<spring-redis.version>6.2.11</spring-redis.version>
<spring-security.version>6.2.8</spring-security.version>
<spring-security.version>6.3.8</spring-security.version>
<jedis.version>5.1.5</jedis.version>
<jjwt.version>0.12.5</jjwt.version>
<slf4j.version>2.0.13</slf4j.version>
@ -57,7 +58,7 @@
<rat.version>0.10</rat.version> <!-- unused -->
<cassandra.version>4.17.0</cassandra.version>
<metrics.version>4.2.25</metrics.version>
<cassandra-all.version>3.11.17</cassandra-all.version> <!-- tools -->
<cassandra-all.version>5.0.4</cassandra-all.version> <!-- tools -->
<guava.version>33.1.0-jre</guava.version>
<caffeine.version>3.1.8</caffeine.version>
<commons-lang3.version>3.14.0</commons-lang3.version>
@ -74,7 +75,7 @@
<jackson-databind.version>2.17.2</jackson-databind.version>
<fasterxml-classmate.version>1.7.0</fasterxml-classmate.version>
<auth0-jwt.version>4.4.0</auth0-jwt.version>
<json-schema-validator.version>2.2.14</json-schema-validator.version>
<json-schema-validator.version>1.5.6</json-schema-validator.version>
<milo.version>0.6.12</milo.version>
<californium.version>3.12.1</californium.version>
<leshan.version>2.0.0-M15</leshan.version>
@ -102,7 +103,7 @@
<jts.version>1.19.0</jts.version>
<bouncycastle.version>1.78.1</bouncycastle.version>
<winsw.version>2.0.1</winsw.version>
<postgresql.driver.version>42.7.3</postgresql.driver.version>
<postgresql.driver.version>42.7.5</postgresql.driver.version>
<sonar.exclusions>org/thingsboard/server/gen/**/*,
org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
</sonar.exclusions>
@ -112,7 +113,7 @@
<!-- IMPORTANT: If you change the version of the kafka client, make sure to synchronize our overwritten implementation of the
org.apache.kafka.common.network.NetworkReceive class in the application module. It addresses the issue https://issues.apache.org/jira/browse/KAFKA-4090.
Here is the source to track https://github.com/apache/kafka/tree/trunk/clients/src/main/java/org/apache/kafka/common/network -->
<kafka.version>3.7.1</kafka.version>
<kafka.version>3.7.2</kafka.version>
<bucket4j.version>8.10.1</bucket4j.version>
<antlr.version>3.5.3</antlr.version>
<snakeyaml.version>2.2</snakeyaml.version>
@ -1163,6 +1164,13 @@
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<!-- Vulnerability fix - transitive dependency from Spring Boot, remove after Spring Boot upgrade -->
<dependency>
<groupId>net.minidev</groupId>
<artifactId>json-smart</artifactId>
<version>${net.minidev.json-smart}</version>
</dependency>
<!-- ...Vulnerability fix - transitive dependency from Spring Boot, remove after Spring Boot upgrade -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
@ -1183,6 +1191,18 @@
<artifactId>spring-security-oauth2-jose</artifactId>
<version>${spring-security.version}</version>
</dependency>
<!-- Vulnerability fix - transitive dependency from Spring Boot, remove after Spring Boot upgrade -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring-security.version}</version>
</dependency>
<!-- ... Vulnerability fix - transitive dependency from Spring Boot, remove after Spring Boot upgrade -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
@ -1600,15 +1620,9 @@
<version>${auth0-jwt.version}</version>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${json-schema-validator.version}</version>
<exclusions>
<exclusion>
<groupId>com.sun.mail</groupId>
<artifactId>mailapi</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.leshan</groupId>
@ -1827,11 +1841,6 @@
<artifactId>cassandra-all</artifactId>
<version>${cassandra-all.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-thrift</artifactId>
<version>${cassandra-all.version}</version>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>

17
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java

@ -64,7 +64,7 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME;
@Slf4j
public class TbMsgDeduplicationNode implements TbNode {
public static final int TB_MSG_DEDUPLICATION_RETRY_DELAY = 10;
public static final long TB_MSG_DEDUPLICATION_RETRY_DELAY = 10L;
private TbMsgDeduplicationNodeConfiguration config;
@ -217,16 +217,17 @@ public class TbMsgDeduplicationNode implements TbNode {
}
private void enqueueForTellNextWithRetry(TbContext ctx, TbMsg msg, int retryAttempt) {
if (config.getMaxRetries() > retryAttempt) {
if (retryAttempt <= config.getMaxRetries()) {
ctx.enqueueForTellNext(msg, TbNodeConnectionType.SUCCESS,
() -> {
log.trace("[{}][{}][{}] Successfully enqueue deduplication result message!", ctx.getSelfId(), msg.getOriginator(), retryAttempt);
},
() -> log.trace("[{}][{}][{}] Successfully enqueue deduplication result message!", ctx.getSelfId(), msg.getOriginator(), retryAttempt),
throwable -> {
log.trace("[{}][{}][{}] Failed to enqueue deduplication output message due to: ", ctx.getSelfId(), msg.getOriginator(), retryAttempt, throwable);
ctx.schedule(() -> {
enqueueForTellNextWithRetry(ctx, msg, retryAttempt + 1);
}, TB_MSG_DEDUPLICATION_RETRY_DELAY, TimeUnit.SECONDS);
if (retryAttempt < config.getMaxRetries()) {
ctx.schedule(() -> enqueueForTellNextWithRetry(ctx, msg, retryAttempt + 1), TB_MSG_DEDUPLICATION_RETRY_DELAY, TimeUnit.SECONDS);
} else {
log.trace("[{}][{}] Max retries [{}] exhausted. Dropping deduplication result message [{}]",
ctx.getSelfId(), msg.getOriginator(), config.getMaxRetries(), msg.getId());
}
});
}
}

3
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java

@ -181,7 +181,8 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest {
SslContext actualSslContext = mqttClientConfig.getValue().getSslContext();
assertThat(actualSslContext)
.usingRecursiveComparison()
.ignoringFields("ctx", "ctxLock", "sessionContext.context.ctx", "sessionContext.context.ctxLock")
.ignoringFields("ctx", "ctxLock", "sessionContext.context.ctx", "sessionContext.context.ctxLock",
"sslContext")
.isEqualTo(SslContextBuilder.forClient().build());
}

76
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java

@ -62,11 +62,13 @@ import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -411,6 +413,80 @@ public class TbMsgDeduplicationNodeTest extends AbstractRuleNodeUpgradeTest {
Assertions.assertEquals(msgWithLatestTsInSecondPack.getType(), actualMsg.getType());
}
@Test
public void given_maxRetriesIsZero_when_enqueueFails_then_noRetriesIsScheduled() throws TbNodeException, ExecutionException, InterruptedException {
int wantedNumberOfTellSelfInvocation = 1;
int msgCount = 1;
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation);
invokeTellSelf(wantedNumberOfTellSelfInvocation);
// Given
when(ctx.getQueueName()).thenReturn(DataConstants.MAIN_QUEUE_NAME);
config.setInterval(deduplicationInterval);
config.setStrategy(DeduplicationStrategy.FIRST);
config.setMaxPendingMsgs(msgCount);
config.setMaxRetries(0);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
DeviceId deviceId = new DeviceId(UUID.randomUUID());
long currentTimeMillis = System.currentTimeMillis();
doAnswer(invocation -> {
Consumer<Throwable> failureCallback = invocation.getArgument(3);
failureCallback.accept(new RuntimeException("Simulated failure"));
return null;
}).when(ctx).enqueueForTellNext(any(), eq(TbNodeConnectionType.SUCCESS), any(), any());
TbMsg msg = createMsg(deviceId, currentTimeMillis + 1);
node.onMsg(ctx, msg);
awaitTellSelfLatch.await();
verify(ctx).enqueueForTellNext(any(), eq(TbNodeConnectionType.SUCCESS), any(), any());
verify(ctx, never()).schedule(any(), anyLong(), any());
}
@Test
public void given_maxRetriesIsSetToOne_when_enqueueFails_then_onlyOneRetryIsScheduled() throws TbNodeException, ExecutionException, InterruptedException {
int wantedNumberOfTellSelfInvocation = 1;
int msgCount = 1;
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation);
invokeTellSelf(wantedNumberOfTellSelfInvocation);
when(ctx.getQueueName()).thenReturn(DataConstants.MAIN_QUEUE_NAME);
config.setInterval(deduplicationInterval);
config.setStrategy(DeduplicationStrategy.FIRST);
config.setMaxPendingMsgs(msgCount);
config.setMaxRetries(1);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
DeviceId deviceId = new DeviceId(UUID.randomUUID());
long currentTimeMillis = System.currentTimeMillis();
doAnswer(invocation -> {
Consumer<Throwable> failureCallback = invocation.getArgument(3);
failureCallback.accept(new RuntimeException("Simulated failure"));
return null;
}).when(ctx).enqueueForTellNext(any(), eq(TbNodeConnectionType.SUCCESS), any(), any());
TbMsg msg = createMsg(deviceId, currentTimeMillis + 1);
node.onMsg(ctx, msg);
awaitTellSelfLatch.await();
ArgumentCaptor<Runnable> retryRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(ctx).schedule(retryRunnableCaptor.capture(), eq(TbMsgDeduplicationNode.TB_MSG_DEDUPLICATION_RETRY_DELAY), eq(TimeUnit.SECONDS));
retryRunnableCaptor.getValue().run();
// Verify total enqueue attempts (initial + retry)
verify(ctx, times(2)).enqueueForTellNext(any(), eq(TbNodeConnectionType.SUCCESS), any(), any());
// No more retries scheduled after reaching maxRetries
verify(ctx).schedule(any(), eq(TbMsgDeduplicationNode.TB_MSG_DEDUPLICATION_RETRY_DELAY), eq(TimeUnit.SECONDS));
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(

4
tools/pom.xml

@ -55,10 +55,6 @@
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId>
</dependency>
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-thrift</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>

6
tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java

@ -59,7 +59,7 @@ public class WriterBuilder {
public static CQLSSTableWriter getTsWriter(File dir) {
return CQLSSTableWriter.builder()
.inDirectory(dir)
.inDirectory(dir.getAbsolutePath())
.forTable(tsSchema)
.using("INSERT INTO thingsboard.ts_kv_cf (entity_type, entity_id, key, partition, ts, bool_v, str_v, long_v, dbl_v, json_v) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@ -68,7 +68,7 @@ public class WriterBuilder {
public static CQLSSTableWriter getLatestWriter(File dir) {
return CQLSSTableWriter.builder()
.inDirectory(dir)
.inDirectory(dir.getAbsolutePath())
.forTable(latestSchema)
.using("INSERT INTO thingsboard.ts_kv_latest_cf (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
@ -77,7 +77,7 @@ public class WriterBuilder {
public static CQLSSTableWriter getPartitionWriter(File dir) {
return CQLSSTableWriter.builder()
.inDirectory(dir)
.inDirectory(dir.getAbsolutePath())
.forTable(partitionSchema)
.using("INSERT INTO thingsboard.ts_kv_partitions_cf (entity_type, entity_id, key, partition) " +
"VALUES (?, ?, ?, ?)")

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html

@ -116,7 +116,7 @@
</mat-cell>
</ng-container>
<mat-header-row class="mat-row-select"
*matHeaderRowDef="['name', 'entityType', 'target', 'type', 'key', 'actions']; sticky: true"></mat-header-row>
*matHeaderRowDef="['name', 'entityType', 'target', 'type', 'key', 'actions']"></mat-header-row>
<mat-row
*matRowDef="let argument; columns: ['name', 'entityType', 'target', 'type', 'key', 'actions']"></mat-row>
</table>

4
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -59,7 +59,6 @@ import { AppState } from '@core/core.state';
export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
eventTypeValue: EventType | DebugEventType;
hideClearEventAction = false;
private filterParams: FilterEventBody = {};
private filterColumns: FilterEntityColumn[] = [];
@ -95,7 +94,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private cd: ChangeDetectorRef,
private store: Store<AppState>,
public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>) {
private debugEventSelected?: EventEmitter<EventBody>,
public hideClearEventAction = false) {
super();
this.loadDataOnInit = false;
this.tableTitle = '';

8
ui-ngx/src/app/modules/home/components/event/event-table.component.ts

@ -58,6 +58,9 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
@Input()
debugEventTypes: Array<DebugEventType>;
@Input()
hideClearEventAction: boolean = false;
activeValue = false;
dirtyValue = false;
entityIdValue: EntityId;
@ -147,12 +150,13 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.cd,
this.store,
this.functionTestButtonLabel,
this.debugEventSelected
this.debugEventSelected,
this.hideClearEventAction
);
}
ngAfterViewInit() {
this.isEmptyData$ = this.entitiesTable.dataSource.isEmpty().subscribe(value => this.eventTableConfig.hideClearEventAction = value);
this.isEmptyData$ = this.entitiesTable.dataSource.isEmpty().subscribe(value => this.eventTableConfig.hideClearEventAction = value || this.hideClearEventAction);
}
ngOnDestroy() {

3
ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html

@ -116,11 +116,12 @@
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>rule-node-config.polygon-definition</mat-label>
<input matInput formControlName="polygonsDefinition" required>
<mat-icon matSuffix class="margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.polygon-definition-hint' | translate }}">
help
</mat-icon>
<mat-hint></mat-hint>
<mat-error *ngIf="geoActionConfigForm.get('polygonsDefinition').hasError('required')">
{{ 'rule-node-config.polygon-definition-required' | translate }}
</mat-error>

2
ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html

@ -77,7 +77,7 @@
<mat-form-field floatLabel="always" class="mat-block flex-1">
<mat-label translate>rule-node-config.key-field-input</mat-label>
<input matInput formControlName="key" required/>
<mat-icon class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon" matSuffix
<mat-icon class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon" matSuffix
color="primary"
matTooltip="{{ 'rule-node-config.math-templatization-tooltip' | translate }}">help</mat-icon>
<mat-error *ngIf="mathFunctionConfigForm.get('result.key').hasError('required')" translate>

3
ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html

@ -19,9 +19,10 @@
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>rule-node-config.custom-table-name</mat-label>
<input required matInput formControlName="tableName">
<mat-icon class="help-icon margin-8 cursor-pointer"
<mat-icon class="help-icon m-2 cursor-pointer"
aria-hidden="false"
aria-label="help-icon"
color="primary"
matSuffix
matTooltip="{{ 'rule-node-config.custom-table-hint' | translate }}">
help

3
ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html

@ -76,9 +76,10 @@
requiredText="{{ 'rule-node-config.default-ttl-required' | translate }}"
minErrorText="{{ 'rule-node-config.min-default-ttl-message' | translate }}"
formControlName="defaultTTL">
<mat-icon class="mr-2 cursor-pointer"
<mat-icon class="help-icon mr-2 cursor-pointer"
aria-hidden="false"
aria-label="help-icon"
color="primary"
matSuffix
matTooltip="{{ 'rule-node-config.default-ttl-hint' | translate }}">
help

2
ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html

@ -64,7 +64,7 @@
*ngIf="argumentControl.get('type').value && argumentControl.get('type').value !== ArgumentType.CONSTANT">
<mat-label translate>rule-node-config.argument-key-field-input</mat-label>
<input [formControl]="argumentControl.get('key')" matInput required/>
<mat-icon class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon" matSuffix
<mat-icon class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon" matSuffix
color="primary"
matTooltip="{{ 'rule-node-config.math-templatization-tooltip' | translate }}">
help

2
ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html

@ -60,7 +60,7 @@
[emptyInputPlaceholder]="'rule-node-config.add-device-profile' | translate"
[filledInputPlaceholder]="'rule-node-config.add-device-profile' | translate"
formControlName="deviceTypes">
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon" color="primary"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon" color="primary"
matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.device-profile' | translate } }}">help</mat-icon>
</tb-entity-subtype-list>
</section>

2
ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html

@ -62,7 +62,7 @@
</div>
</mat-option>
</mat-autocomplete>
<mat-icon class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon" matSuffix
<mat-icon class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon" matSuffix
color="primary"
matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.message-type' | translate } }}">help</mat-icon>
<mat-error *ngIf="chipList.errorState">

2
ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html

@ -53,7 +53,7 @@
</div>
<ng-template #helpIcon>
<mat-icon class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.field-name' | translate } }}">help</mat-icon>
</ng-template>

2
ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html

@ -22,7 +22,7 @@
[placeholder]="'rule-node-config.add-detail' | translate"
[requiredText]="'rule-node-config.entity-details-list-empty' | translate"
formControlName="detailsList">
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.detail' | translate } }}">
help

4
ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html

@ -85,7 +85,7 @@
<mat-error *ngIf="lambdaConfigForm.get('connectionTimeout').hasError('min')">
{{ 'rule-node-config.connection-timeout-min' | translate }}
</mat-error>
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.connection-timeout-hint' | translate }}">help</mat-icon>
</mat-form-field>
@ -98,7 +98,7 @@
<mat-error *ngIf="lambdaConfigForm.get('requestTimeout').hasError('min')">
{{ 'rule-node-config.request-timeout-min' | translate }}
</mat-error>
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.request-timeout-hint' | translate }}">help</mat-icon>
</mat-form-field>

4
ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html

@ -26,14 +26,14 @@
[label]="'rule-node-config.data-keys' | translate"
[placeholder]="'rule-node-config.add-message-field' | translate"
formControlName="messageNames">
<mat-icon matSuffix color="primary" class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix color="primary" class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.field-name' | translate } }}">help</mat-icon>
</tb-string-items-list>
<tb-string-items-list editable subscriptSizing="dynamic"
[label]="'rule-node-config.metadata-keys' | translate"
[placeholder]="'rule-node-config.add-metadata-field' | translate"
formControlName="metadataNames">
<mat-icon matSuffix color="primary" class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix color="primary" class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.field-name' | translate } }}">help</mat-icon>
</tb-string-items-list>
<div tb-hint-tooltip-icon="{{ 'rule-node-config.check-all-keys-tooltip' | translate }}"

2
ui-ngx/src/app/modules/home/components/rule-node/filter/originator-type-config.component.html

@ -24,7 +24,7 @@
[filledInputPlaceholder]="'rule-node-config.add-entity-type' | translate"
[label]="'rule-node-config.select-entity-types' | translate"
required>
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.chip-help' | translate: { inputName: 'rule-node-config.entity-type' | translate } }}">help</mat-icon>
</tb-entity-type-list>

2
ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html

@ -27,7 +27,7 @@
[placeholder]="'rule-node-config.add-key' | translate"
[requiredText]="'rule-node-config.key-val.at-least-one-key-error' | translate"
formControlName="keys">
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.use-regular-expression-hint' | translate }}">
help

6
ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html

@ -25,7 +25,7 @@
<mat-error *ngIf="deduplicationConfigForm.get('interval').hasError('min')">
{{'rule-node-config.interval-min-error' | translate}}
</mat-error>
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.interval-hint' | translate }}">help</mat-icon>
</mat-form-field>
@ -73,7 +73,7 @@
<mat-error *ngIf="deduplicationConfigForm.get('maxPendingMsgs').hasError('min')">
{{'rule-node-config.max-pending-msgs-min-error' | translate}}
</mat-error>
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.max-pending-msgs-hint' | translate }}">help</mat-icon>
</mat-form-field>
@ -89,7 +89,7 @@
<mat-error *ngIf="deduplicationConfigForm.get('maxRetries').hasError('min')">
{{'rule-node-config.max-retries-min-error' | translate}}
</mat-error>
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.max-retries-hint' | translate }}">help</mat-icon>
</mat-form-field>

2
ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html

@ -26,7 +26,7 @@
[placeholder]="'rule-node-config.add-key' | translate"
[requiredText]="'rule-node-config.key-val.at-least-one-key-error' | translate"
formControlName="keys">
<mat-icon matSuffix class="help-icon margin-8 cursor-pointer" aria-hidden="false" aria-label="help-icon"
<mat-icon matSuffix class="help-icon m-2 cursor-pointer" aria-hidden="false" aria-label="help-icon"
color="primary"
matTooltip="{{ 'rule-node-config.use-regular-expression-delete-hint' | translate }}">
help

9
ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts

@ -122,17 +122,10 @@ export class TbPieChart extends TbLatestChart<PieChartSettings> {
seriesData.push(
{id: dataItem.id, value: dataItem.value, name: dataItem.dataKey.label, itemStyle: {color: dataItem.dataKey.color}}
);
if (this.settings.doughnut && enabledDataItems.length > 1) {
seriesData.push({
value: 0, name: '', itemStyle: {color: 'transparent'}, emphasis: {disabled: true}
});
}
}
}
if (this.settings.doughnut) {
for (let i = 1; i < seriesData.length; i += 2) {
seriesData[i].value = this.total / 100;
}
this.latestChartOption.series[0].padAngle = enabledDataItems.length > 1 ? 2 : 0;
}
this.latestChartOption.series[0].data = seriesData;
}

168
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -83,6 +83,10 @@ export interface ScadaSymbolApi {
cssAnimation: (element: Element) => ScadaSymbolAnimation | undefined;
resetCssAnimation: (element: Element) => void;
finishCssAnimation: (element: Element) => void;
connectorAnimation:(element: Element) => ConnectorScadaSymbolAnimation | undefined;
connectorAnimate:(element: Element, path: string, reversedPath: string) => ConnectorScadaSymbolAnimation;
resetConnectorAnimation: (element: Element) => void;
finishConnectorAnimation: (element: Element) => void;
disable: (element: Element | Element[]) => void;
enable: (element: Element | Element[]) => void;
callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) => void;
@ -186,6 +190,8 @@ const tbNamespaceRegex = /<svg.*(xmlns:tb="https:\/\/thingsboard.io\/svg").*>/gm
const tbTagRegex = /tb:tag="([^"]*)"/gms;
const syncTime = Date.now();
const generateElementId = () => {
const id = guid();
const firstChar = id.charAt(0);
@ -485,6 +491,7 @@ export class ScadaSymbolObject {
private settings: ScadaSymbolObjectSettings;
private context: ScadaSymbolContext;
private cssAnimations: CssScadaSymbolAnimations;
private connectorAnimations: ScadaSymbolFlowConnectorAnimations;
private svgShape: Svg;
private box: Box;
@ -604,6 +611,7 @@ export class ScadaSymbolObject {
private init() {
this.cssAnimations = new CssScadaSymbolAnimations(this.svgShape, this.raf);
this.connectorAnimations = new ScadaSymbolFlowConnectorAnimations();
this.context = {
api: {
generateElementId: () => generateElementId(),
@ -615,6 +623,10 @@ export class ScadaSymbolObject {
cssAnimation: this.cssAnimation.bind(this),
resetCssAnimation: this.resetCssAnimation.bind(this),
finishCssAnimation: this.finishCssAnimation.bind(this),
connectorAnimation: this.connectorAnimation.bind(this),
connectorAnimate: this.connectorAnimate.bind(this),
resetConnectorAnimation: this.resetConnectorAnimation.bind(this),
finishConnectorAnimation: this.finishConnectorAnimation.bind(this),
disable: this.disableElement.bind(this),
enable: this.enableElement.bind(this),
callAction: this.callAction.bind(this),
@ -959,6 +971,22 @@ export class ScadaSymbolObject {
this.cssAnimations.finishAnimation(element);
}
private connectorAnimate(element: Element, path: string, reversedPath: string): ConnectorScadaSymbolAnimation {
return this.connectorAnimations.animate(element, path, reversedPath);
}
private connectorAnimation(element: Element): ConnectorScadaSymbolAnimation | undefined {
return this.connectorAnimations.animation(element);
}
private resetConnectorAnimation(element: Element) {
this.connectorAnimations.resetAnimation(element);
}
private finishConnectorAnimation(element: Element) {
this.connectorAnimations.finishAnimation(element);
}
private disableElement(e: Element | Element[]) {
this.elements(e).forEach(element => {
element.attr({'pointer-events': 'none'});
@ -1108,6 +1136,20 @@ interface ScadaSymbolAnimation {
}
const scadaSymbolConnectorFlowAnimationId = 'scadaSymbolConnectorFlowAnimation';
type StrokeLineCap = 'butt' | 'round '| 'square';
interface ConnectorScadaSymbolAnimation {
play(): void;
stop(): void;
finish(): void;
flowAppearance(width: number, color: string, lineCap: StrokeLineCap, dashWidth: number, dashGap: number): ConnectorScadaSymbolAnimation;
duration(speed: number): ConnectorScadaSymbolAnimation;
direction(direction: boolean): ConnectorScadaSymbolAnimation;
}
class CssScadaSymbolAnimations {
constructor(private svgShape: Svg,
private raf: RafService) {}
@ -1159,6 +1201,132 @@ class CssScadaSymbolAnimations {
}
}
class ScadaSymbolFlowConnectorAnimations {
constructor() {}
public animate(element: Element, path = '', reversedPath = ''): ConnectorScadaSymbolAnimation {
this.checkOldAnimation(element);
return this.setupAnimation(element, this.createAnimation(element, path, reversedPath));
}
public animation(element: Element): ConnectorScadaSymbolAnimation | undefined {
return element.remember(scadaSymbolConnectorFlowAnimationId);
}
public resetAnimation(element: Element) {
const animation: ConnectorScadaSymbolAnimation = element.remember(scadaSymbolConnectorFlowAnimationId);
if (animation) {
animation.stop();
element.remember(scadaSymbolConnectorFlowAnimationId, null);
}
}
public finishAnimation(element: Element) {
const animation: ConnectorScadaSymbolAnimation = element.remember(scadaSymbolConnectorFlowAnimationId);
if (animation) {
animation.finish();
element.remember(scadaSymbolConnectorFlowAnimationId, null);
}
}
private setupAnimation(element: Element, animation: ConnectorScadaSymbolAnimation): ConnectorScadaSymbolAnimation {
element.remember(scadaSymbolConnectorFlowAnimationId, animation);
return animation;
}
private checkOldAnimation(element: Element) {
const previousAnimation: ConnectorScadaSymbolAnimation = element.remember(scadaSymbolConnectorFlowAnimationId);
if (previousAnimation) {
previousAnimation.finish();
}
}
private createAnimation(element: Element, path: string, reversedPath: string): ConnectorScadaSymbolAnimation {
return new FlowConnectorAnimation(element, path, reversedPath);
}
}
class FlowConnectorAnimation implements ConnectorScadaSymbolAnimation {
private readonly _path: string;
private readonly _reversedPath: string;
private readonly _animation: Element;
private _duration: number = 1;
private _lineColor: string = '#C8DFF7';
private _lineWidth: number = 4;
private _strokeLineCap: StrokeLineCap = 'butt';
private _dashWidth: number = 10;
private _dashGap: number = 10;
private _direction: boolean = true;
constructor(private element: Element,
path: string,
pathReversed: string) {
this._path = path;
this._reversedPath = pathReversed;
const dashArray = `${this._dashWidth} ${this._dashGap}`;
const values = `${this._dashWidth + this._dashGap};0`;
this._animation = SVG(
`<path d="${this._path}" stroke-dasharray="${dashArray}" stroke-linecap="${this._strokeLineCap}" fill="none" stroke="${this._lineColor}" stroke-width="${this._lineWidth}">` +
`<animate attributeName="stroke-dashoffset" values="${values}" dur="${this._duration}s" begin="indefinite" calcMode="linear" repeatCount="indefinite"></animate></path>`
);
}
public play() {
if (!this.element.node.childElementCount) {
this.element.add(this._animation);
}
const animateElement = this.element.node.getElementsByTagName('animate')[0];
const offset = ((Date.now() - syncTime) % 1000) * -1;
(animateElement as SVGAnimationElement).beginElementAt(offset);
}
public stop() {
const animateElement = this.element.node.getElementsByTagName('animate')[0];
(animateElement as SVGAnimationElement)?.endElement();
}
public finish() {
this.element.findOne('path')?.remove();
}
public flowAppearance(width: number, color: string, linecap: StrokeLineCap, dashWidth: number, dashGap: number): this {
const totalLength = (this._animation.node as SVGPathElement).getTotalLength();
let offset = 0;
if ((totalLength % 100) !== 0) {
const clientWidth = totalLength < 100 ? 100 : this.element.node.ownerSVGElement.clientWidth;
const clientWidthDash = clientWidth / (dashWidth + dashGap);
const totalLengthDash = totalLength / clientWidthDash;
offset = ((dashWidth + dashGap) - totalLengthDash) / 2;
}
this._lineColor = color;
this._lineWidth = width;
this._strokeLineCap = linecap;
this._dashWidth = dashWidth - offset;
this._dashGap = dashGap - offset;
const dashArray = `${this._dashWidth} ${this._dashGap}`;
const values = `${this._dashWidth + (this._dashGap || this._dashWidth)};0`;
this._animation.stroke({width, color, linecap, dasharray: dashArray});
this._animation.findOne('animate').attr('values', values);
return this;
}
public duration(speed: number): this {
this._duration = speed;
this._animation.findOne('animate').attr('dur', `${speed}s`);
return this;
}
public direction(direction: boolean): this {
this._direction = direction;
this._animation.attr('d', direction ? this._path : this._reversedPath);
return this;
}
}
interface ScadaSymbolAnimationKeyframe {
stop: string;
style: any;

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html

@ -30,6 +30,12 @@
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.hint</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="hint" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.group-title</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts

@ -104,6 +104,7 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
{
id: [this.property.id, [Validators.required]],
name: [this.property.name, [Validators.required]],
hint: [this.property.hint, []],
group: [this.property.group, []],
type: [this.property.type, [Validators.required]],
arrayItemType: [this.property.arrayItemType, [Validators.required]],

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html

@ -91,9 +91,15 @@
<div [formGroup]="propertiesFormGroup" class="tb-form-row space-between overflow-auto" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch && propertyRow.switch.visible"
class="mat-slide fixed-title-width margin" formControlName="{{ propertyRow.switch.id }}">
{{ propertyRow.label | customTranslate }}
<div tb-hint-tooltip-icon="{{ propertyRow.hint | customTranslate }}">
{{ propertyRow.label | customTranslate }}
</div>
</mat-slide-toggle>
<div *ngIf="!propertyRow.switch" class="fixed-title-width fixed-title-height">{{ propertyRow.label | customTranslate }}</div>
<div *ngIf="!propertyRow.switch" class="fixed-title-width fixed-title-height">
<div tb-hint-tooltip-icon="{{ propertyRow.hint | customTranslate }}">
{{ propertyRow.label | customTranslate }}
</div>
</div>
<div *ngIf="propertyRow.properties.length" class="tb-flex" [class]="propertyRow.propertiesRowClass">
<ng-container *ngFor="let property of propertyRow.properties">
<ng-container *ngIf="property.visible">

2
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div [formGroup]="ruleNodeConfigFormGroup">
<div [formGroup]="ruleNodeConfigFormGroup" class="rule-node-config">
<ng-container #definedConfigContent></ng-container>
<div class="tb-rulenode-directive-error" *ngIf="definedDirectiveError">{{definedDirectiveError}}</div>
<tb-json-object-edit *ngIf="!useDefinedDirective()" #jsonObjectEditComponent

53
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.scss

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
display: block;
@import './../scss/constants';
.rule-node-config {
tb-json-object-edit.tb-rule-node-configuration-json {
display: block;
height: 300px;
@ -26,4 +26,53 @@
font-weight: 400;
color: rgb(221, 44, 0);
}
.tb-error {
letter-spacing: 0.25px;
color: var(--mdc-theme-error, #f44336);
}
.tb-required::after {
content: "*";
font-size: 16px;
color: #000000de;
}
.help-icon {
color: #000000;
opacity: .38;
padding: unset;
&:hover {
color: $tb-primary-color;
opacity: unset;
}
}
.same-width-component-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 16px;
@media #{$mat-xs} {
flex-direction: column;
flex-wrap: wrap;
gap: 8px;
}
> * {
flex: 1;
}
.flex-2 {
flex: 2;
}
.third-width {
max-width: 32%;
@media #{$mat-xs} {
max-width: 100%;
}
}
}
}

10
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts

@ -19,11 +19,13 @@ import {
ComponentRef,
EventEmitter,
forwardRef,
HostBinding,
Input,
OnDestroy,
Output,
ViewChild,
ViewContainerRef
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {
ControlValueAccessor,
@ -52,14 +54,16 @@ import { RuleChainType } from '@shared/models/rule-chain.models';
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RuleNodeConfigComponent),
multi: true
}]
}],
encapsulation: ViewEncapsulation.None
})
export class RuleNodeConfigComponent implements ControlValueAccessor, OnDestroy {
@ViewChild('definedConfigContent', {read: ViewContainerRef, static: true}) definedConfigContainer: ViewContainerRef;
@ViewChild('jsonObjectEditComponent') jsonObjectEditComponent: JsonObjectEditComponent;
@HostBinding('style.display') readonly styleDisplay = 'block';
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;

64
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts

@ -1129,6 +1129,8 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags
const scadaSymbolAnimationLink = HelpLinks.linksMap.scadaSymbolDevAnimation;
const scadaSymbolAnimation = `<a href="${scadaSymbolAnimationLink}" target="_blank">ScadaSymbolAnimation</a>`;
const connectorScadaSymbolAnimationLink = HelpLinks.linksMap.scadaSymbolDevConnectorAnimation;
const connectorScadaSymbolAnimation = `<a href="${connectorScadaSymbolAnimationLink}" target="_blank">ConnectorScadaSymbolAnimation</a>`;
const properties: TbEditorCompletion = {
meta: 'object',
@ -1229,6 +1231,68 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags
},
]
},
connectorAnimate: {
meta: 'function',
description: 'Finishes any previous connector animation and starts a new connector animation for the SVG element along the specified path.',
args: [
{
name: 'element',
description: 'SVG element',
type: 'Element'
},
{
name: 'path',
description: 'Path defining the animation trajectory',
type: 'string'
},
{
name: 'reversedPath',
description: 'Path for the reversed animation trajectory',
type: 'string'
}
],
return: {
description: `Instance of ${connectorScadaSymbolAnimation} class with API to setup and control the connector animation.`,
type: connectorScadaSymbolAnimation
}
},
connectorAnimation: {
meta: 'function',
description: 'Gets the current connector animation applied to the SVG element.',
args: [
{
name: 'element',
description: 'SVG element',
type: 'Element'
}
],
return: {
description: `Instance of ${connectorScadaSymbolAnimation} class with API to setup and control the connector animation, or undefined if no animation is applied.`,
type: 'ConnectorScadaSymbolAnimation | undefined'
}
},
resetConnectorAnimation: {
meta: 'function',
description: 'Stops the connector animation if any and restores the SVG element to its initial state, removes the connector animation instance.',
args: [
{
name: 'element',
description: 'SVG element',
type: 'Element'
}
]
},
finishConnectorAnimation: {
meta: 'function',
description: 'Finishes the connector animation if any, updates the SVG element state according to the end animation values, removes the connector animation instance.',
args: [
{
name: 'element',
description: 'SVG element',
type: 'Element'
}
]
},
generateElementId: {
meta: 'function',
description: 'Generates new unique element id.',

13
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts

@ -190,15 +190,22 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
this.searchText = '';
if (value != null) {
if (typeof value === 'string') {
this.dashboardService.getDashboardInfo(value).subscribe(
(dashboard) => {
this.dashboardService.getDashboardInfo(value, {ignoreLoading: true, ignoreErrors: true}).subscribe({
next: (dashboard) => {
this.modelValue = this.useIdValue ? dashboard.id.id : dashboard;
if (this.useDashboardLink) {
this.dashboardURL = getEntityDetailsPageURL(this.modelValue as string, EntityType.DASHBOARD);
}
this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: false});
},
error: () => {
this.modelValue = null;
this.selectDashboardFormGroup.get('dashboard').patchValue('', {emitEvent: false});
if (this.required) {
this.propagateChange(this.modelValue);
}
}
);
});
} else {
this.modelValue = this.useIdValue ? value.id.id : value;
this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: false});

1
ui-ngx/src/app/shared/models/constants.ts

@ -194,6 +194,7 @@ export const HelpLinks = {
scada: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada`,
scadaSymbolDev: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/`,
scadaSymbolDevAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolanimation`,
scadaSymbolDevConnectorAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#connectorscadasymbolanimation`,
domains: `${helpBaseUrl}/docs${docPlatformPrefix}/domains`,
mobileApplication: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/applications/`,
mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`,

3
ui-ngx/src/app/shared/models/dynamic-form.models.ts

@ -87,6 +87,7 @@ export type PropertyConditionFunction = (property: FormProperty, model: any) =>
export interface FormPropertyBase {
id: string;
name: string;
hint?: string;
group?: string;
type: FormPropertyType;
default: any;
@ -237,6 +238,7 @@ export interface FormPropertyContainerBase {
}
export interface FormPropertyRow extends FormPropertyContainerBase {
hint?: string;
properties?: FormProperty[];
switch?: FormProperty;
rowClass?: string;
@ -362,6 +364,7 @@ const toPropertyContainers = (properties: FormProperty[],
if (!propertyRow) {
propertyRow = {
label: property.name,
hint: property.hint,
type: FormPropertyContainerType.row,
properties: [],
rowClass: property.rowClass,

14
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3438,15 +3438,15 @@
"arrow-presence": "Arrow presence",
"arrow-presence-hint": "Indicates whether arrow is present in connector.",
"arrow-present": "Arrow present",
"arrow-direction": "Arrow/Animation direction",
"arrow-direction": "Flow direction",
"arrow-direction-hint": "Indicates flow direction.",
"animation-direction": "Flow animation direction",
"animation-direction-hint": "Indicates animation flow direction.",
"flow-animation": "Flow animation",
"flow-animation-hint": "Indicates whether animation is present in connector.",
"flow-animation": "Flow presence",
"flow-animation-hint": "Indicates whether fluid is flowing in connector.",
"flow": "Flow",
"flow-style": "Flow style",
"flow-dash-cap": "Flow dash cap",
"flow-line": "Line",
"flow-line-style": "Line style",
"flow-style-hint": "Set the Dash and Gap values so that their sum is divisible by 100 without a remainder for perfect animation synchronization.",
"flow-dash-cap": "Dash cap",
"dash-cap-butt": "Butt",
"dash-cap-round": "Round",
"dash-cap-square": "Square",

Loading…
Cancel
Save