Browse Source

Install SCADA symbols from data folder. UI: SCADA editor -> Add Zoom In/Out buttons, improve tooltips.

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
2500bb2e1c
  1. 316
      application/src/main/data/json/system/scada_symbols/level_and_fan.svg
  2. 4
      application/src/main/data/json/system/widget_bundles/scada_symbols.json
  3. 13
      application/src/main/data/json/system/widget_types/scada_symbol.json
  4. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 5
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  6. 96
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  8. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
  9. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  10. 5
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  11. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  12. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  13. 43
      dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java
  14. 2
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
  15. 9
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  16. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts
  17. 14
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  18. 4
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts
  19. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  20. 12
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  21. 11
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts
  22. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  23. 5
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  24. 11
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts
  25. 3
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html
  26. 4
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.scss
  27. 4
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts
  28. 2
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  29. 46
      ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.html
  30. 62
      ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.ts
  31. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  32. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  33. 19
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html
  34. 12
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts
  35. 37
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html
  36. 193
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss
  37. 45
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts
  38. 124
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts
  39. 2
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts
  40. 4
      ui-ngx/src/app/shared/models/widget.models.ts
  41. 5
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  42. 150
      ui-ngx/src/styles.scss

316
application/src/main/data/json/system/scada_symbols/level_and_fan.svg

@ -0,0 +1,316 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" version="1.1" viewBox="0 0 100 100">
<tb:metadata xmlns=""><![CDATA[{
"title": "Level and Fan",
"description": "Level and Fan Symbol",
"searchTags": [
"level",
"fan"
],
"stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}",
"tags": [
{
"tag": "fan",
"stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n var level = ctx.values.level; \n var minLevel = ctx.properties.minLevel; \n var maxLevel = ctx.properties.maxLevel;\n\n var speed = (level - minLevel) / (maxLevel - minLevel);\n speed = Math.max(0, Math.min(1, speed))*2; \n\n var t = element.timeline();\n if (!t.active()) {\n ctx.api.animate(element, 1000).ease('-').rotate(360).loop();\n t = element.timeline();\n }\n t.speed(speed);\n} else {\n element.timeline().stop();\n}\n",
"actions": null
},
{
"tag": "level",
"stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});",
"actions": null
},
{
"tag": "levelDownButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
},
{
"tag": "levelTitle",
"stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}",
"actions": null
},
{
"tag": "levelUpButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
},
{
"tag": "levelValue",
"stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'levelValueClick');"
}
}
},
{
"tag": "offButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n"
}
}
},
{
"tag": "onButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});\n\n"
}
}
},
{
"tag": "ramka",
"stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n if (!element.timeline().active()) {\n ctx.api.animate(element, 1000).scale(1.1).loop(0, true);\n }\n} else {\n element.timeline().stop();\n}",
"actions": null
}
],
"behavior": [
{
"id": "level",
"name": "Level",
"hint": null,
"type": "value",
"valueType": "DOUBLE",
"defaultValue": 10,
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"valueToDataType": null,
"constantValue": null,
"valueToDataFunction": null
},
{
"id": "disabled",
"name": "{i18n:widgets.rpc-state.disabled-state}",
"hint": "{i18n:widgets.rpc-state.disabled-state-hint}",
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": false,
"trueLabel": "",
"falseLabel": "",
"stateLabel": "{i18n:widgets.rpc-state.disabled}",
"valueToDataType": null,
"constantValue": null,
"valueToDataFunction": null
},
{
"id": "on",
"name": "On/Off state",
"hint": null,
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": true,
"trueLabel": "{i18n:widgets.rpc-state.on}",
"falseLabel": "{i18n:widgets.rpc-state.off}",
"stateLabel": "{i18n:widgets.rpc-state.on}",
"valueToDataType": null,
"constantValue": null,
"valueToDataFunction": null
},
{
"id": "onUpdateState",
"name": "{i18n:widgets.rpc-state.turn-on}",
"hint": "{i18n:widgets.rpc-state.turn-on-hint}",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": true
},
{
"id": "offUpdateState",
"name": "{i18n:widgets.rpc-state.turn-off}",
"hint": "{i18n:widgets.rpc-state.turn-off-hint}",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": false
},
{
"id": "levelUpdateState",
"name": "Update level",
"type": "action",
"valueType": "DOUBLE",
"valueToDataType": "VALUE",
"constantValue": 0
},
{
"id": "levelValueClick",
"name": "On level value click",
"type": "widgetAction"
}
],
"properties": [
{
"id": "showLevelTitle",
"name": "Level title",
"type": "switch",
"default": true,
"rowClass": "column-xs"
},
{
"id": "levelTitle",
"name": "Level title",
"type": "text",
"default": "{i18n:widgets.battery-level.value}",
"disableOnProperty": "showLevelTitle",
"fieldClass": "flex"
},
{
"id": "levelTitleFont",
"name": "Level title",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showLevelTitle"
},
{
"id": "levelTitleColor",
"name": "Level title",
"type": "color",
"default": "#000000",
"disableOnProperty": "showLevelTitle"
},
{
"id": "showValue",
"name": "Value",
"type": "switch",
"default": true,
"rowClass": "column-xs"
},
{
"id": "valueUnits",
"name": "Value",
"type": "units",
"default": "",
"disableOnProperty": "showValue",
"fieldClass": "flex"
},
{
"id": "valueDecimals",
"name": "Value",
"type": "number",
"default": 2,
"min": 0,
"max": 15,
"step": 1,
"fieldSuffix": "decimals",
"disableOnProperty": "showValue",
"fieldClass": "flex"
},
{
"id": "valueFont",
"name": "Value",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showValue"
},
{
"id": "valueColor",
"name": "Value",
"type": "color",
"default": "#000000",
"disableOnProperty": "showValue"
},
{
"id": "minLevel",
"name": "Level range",
"subLabel": "min",
"type": "number",
"required": true,
"default": 0,
"min": 0,
"rowClass": "column-xs",
"fieldClass": "flex-xs"
},
{
"id": "maxLevel",
"name": "Level range",
"subLabel": "max",
"type": "number",
"required": true,
"default": 100,
"min": 0,
"fieldClass": "flex-xs"
},
{
"id": "showMinMaxLevel",
"name": "Min/Max label",
"type": "switch",
"default": true
},
{
"id": "minMaxLevelFont",
"name": "Min/Max label",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "minMaxLevelColor",
"name": "Min/Max label",
"type": "color",
"default": "#666",
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "levelBackground",
"name": "Level background",
"subLabel": "Enabled",
"type": "color_settings",
"default": {
"type": "constant",
"color": "#1abb48",
"colorFunction": "return value > 70 ? '#d5280d' : '#1abb48';"
},
"divider": true,
"rowClass": "column-xs"
},
{
"id": "disabledLevelBackground",
"name": "Level background",
"subLabel": "Disabled",
"type": "color",
"default": "#ccc"
}
]
}]]></tb:metadata><rect width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2" tb:tag="rect"/><rect x="8" y="15" width="20" height="81" rx="2" fill="#ececec" stroke="#000" stroke-width="1.0359"/><rect transform="scale(1 -1)" x="8.5" y="-95.5" width="19" height="40" rx="1.5" fill="#1abb48" tb:tag="level"/><text x="32" y="95" fill="#666" font-family="Roboto" font-size="6px" tb:tag="minLevel" xml:space="preserve"><tspan x="31.354307" y="96.626251">min</tspan></text><rect x="10.941" y="49.544" width="14.155" height="12" rx="2.8668" fill="#fff" fill-opacity=".45148" tb:tag="levelValueBackground"/><g font-family="Roboto" font-size="6px">
<text x="18" y="56" dominant-baseline="middle" fill="#000000" text-anchor="middle" tb:tag="levelValue" xml:space="preserve"><tspan>N/A</tspan></text>
<text x="17.636805" y="10.347933" dominant-baseline="middle" fill="#000000" text-anchor="middle" tb:tag="levelTitle" xml:space="preserve"><tspan>Level</tspan></text>
<text x="32" y="20" fill="#666666" tb:tag="maxLevel" xml:space="preserve"><tspan x="29.865496" y="18.716127">max</tspan></text>
</g><path d="m82.143 27.538h-20.499c-1.1045 0-1.9999 0.8954-1.9999 1.9999v20.499c0 1.1045 0.8954 1.9999 1.9999 1.9999h20.499c1.1045 0 1.9999-0.8954 1.9999-1.9999v-20.499c0-1.1045-0.8954-1.9999-1.9999-1.9999zm-20.499 1.4999c0.27613 0 0.49998 0.22385 0.49998 0.49998s-0.22385 0.49998-0.49998 0.49998-0.49998-0.22385-0.49998-0.49998 0.22385-0.49998 0.49998-0.49998zm0 21.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm10.249-0.79997c-5.495 0-9.9495-4.4545-9.9495-9.9495s4.4545-9.9495 9.9495-9.9495 9.9495 4.4545 9.9495 9.9495-4.4545 9.9495-9.9495 9.9495zm10.249 0.79997c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm0-20.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998z" fill="#080d7d" stroke-width=".040031" tb:tag="ramka"/><path d="m80.66 37.983c-0.04956-0.24078-0.15288-0.4126-0.34242-0.56916-1.5148-1.2518-3.6154-1.1677-6.3021 0.25239-0.17533-0.17533-0.36512-0.32353-0.5778-0.45114 1.8517-1.6612 3.5396-2.3586 5.0637-2.0924 0.44934 0.0785 0.7605-0.43637 0.48209-0.79765-0.61795-0.80201-1.3212-1.455-2.1668-2.0119-0.20536-0.13522-0.39987-0.18358-0.64461-0.16032-1.9563 0.18594-3.3822 1.7308-4.2778 4.6347-0.24799 0-0.48697 0.02938-0.72756 0.08955 0.1347-2.484 0.83504-4.1707 2.101-5.0601 0.37324-0.2622 0.22918-0.84629-0.22313-0.90497-1.0041-0.13014-1.9631-0.09459-2.9548 0.10956-0.24078 0.04956-0.4126 0.15288-0.56916 0.34242-1.2518 1.5148-1.1677 3.6154 0.25239 6.3021-0.17533 0.17533-0.32353 0.36512-0.45114 0.5778-1.6612-1.8517-2.3586-3.5396-2.0924-5.0637 0.0785-0.44934-0.43637-0.7605-0.79765-0.48213-0.80201 0.61795-1.455 1.3212-2.0119 2.1668-0.13522 0.20536-0.18358 0.39986-0.16032 0.64461 0.18594 1.9563 1.7308 3.3822 4.6347 4.2778 0 0.24799 0.02938 0.48697 0.08955 0.72756-2.484-0.1347-4.1707-0.83504-5.0601-2.101-0.2622-0.37324-0.84629-0.22918-0.90497 0.22313-0.13014 1.0041-0.09459 1.9631 0.10956 2.9548 0.04956 0.24078 0.15288 0.4126 0.34242 0.56916 1.5148 1.2518 3.6154 1.1677 6.3021-0.25239 0.17533 0.17533 0.36512 0.32353 0.5778 0.45114-1.8517 1.6612-3.5396 2.3586-5.0637 2.0924-0.44934-0.0785-0.7605 0.43637-0.48209 0.79765 0.61795 0.80201 1.3212 1.455 2.1668 2.0119 0.20536 0.13522 0.39986 0.18358 0.64461 0.16032 1.9563-0.18594 3.3822-1.7308 4.2778-4.6347 0.24799 0 0.48697-0.02938 0.72756-0.08959-0.1347 2.484-0.83504 4.1707-2.101 5.0601-0.37324 0.2622-0.22918 0.84629 0.22313 0.90497 1.0041 0.13014 1.9631 0.09459 2.9548-0.10956 0.24078-0.04956 0.4126-0.15288 0.56916-0.34242 1.2518-1.5148 1.1677-3.6154-0.25239-6.3021 0.17533-0.17533 0.32353-0.36512 0.45114-0.5778 1.6612 1.8517 2.3586 3.5396 2.0924 5.0637-0.0785 0.44934 0.43637 0.7605 0.79765 0.48209 0.80201-0.61795 1.455-1.3212 2.0119-2.1668 0.13522-0.20536 0.18358-0.39987 0.16032-0.64461-0.18594-1.9563-1.7308-3.3822-4.6347-4.2778 0-0.24799-0.02938-0.48697-0.08955-0.72756 2.484 0.1347 4.1707 0.83504 5.0601 2.101 0.2622 0.37324 0.84629 0.22918 0.90497-0.22313 0.13014-1.004 0.09459-1.963-0.10956-2.9547zm-8.7657 4.3043c-1.3806 0-2.4999-1.1193-2.4999-2.4999s1.1193-2.4999 2.4999-2.4999 2.4999 1.1193 2.4999 2.4999-1.1193 2.4999-2.4999 2.4999z" fill="#7d081e" stroke-width=".040031" tb:tag="fan"/><g tb:tag="onButton">
<rect x="54.702" y="60.372" width="14.263" height="7.4261" rx="1.5" fill="#12ed19" stroke="#000" tb:tag="onButtonBackground"/>
<text x="61.855518" y="64.49128" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="4.4461px" stroke-width=".74101" text-anchor="middle" xml:space="preserve"><tspan stroke-width=".74101">On</tspan></text>
</g><g tb:tag="offButton">
<rect x="74.367" y="60.311" width="14.263" height="7.4261" rx="1.5" fill="#ed121f" stroke="#000" tb:tag="offButtonBackground"/>
<text x="81.365944" y="64.518349" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="4.4461px" stroke-width=".74101" text-anchor="middle" xml:space="preserve"><tspan stroke-width=".74101">Off</tspan></text>
</g><g stroke="#000" stroke-miterlimit="1" tb:tag="levelUpButton">
<rect x="34.97" y="39.377" width="9.5005" height="12.584" rx=".75" fill="none"/>
<path transform="matrix(.4753 -.2938 .2938 .4753 4.8821 31.04)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/>
</g><g stroke="#000" stroke-miterlimit="1" tb:tag="levelDownButton">
<rect x="34.97" y="59.039" width="9.5005" height="12.584" rx=".75" fill="none"/>
<path transform="matrix(-.47514 .29405 -.29405 -.47514 74.568 79.771)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

4
application/src/main/data/json/system/widget_bundles/scada_symbols.json

@ -7,7 +7,5 @@
"order": 9200,
"name": "SCADA symbols"
},
"widgetTypeFqns": [
"scada_symbol"
]
"widgetTypeFqns": []
}

13
application/src/main/data/json/system/widget_types/scada_symbol.json

File diff suppressed because one or more lines are too long

2
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -150,6 +150,7 @@ public class ThingsboardInstallService {
log.info("Updating system data...");
dataUpdateService.upgradeRuleNodes();
systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.loadSystemScadaSymbols();
installScripts.loadSystemLwm2mResources();
installScripts.loadSystemImages();
if (installScripts.isUpdateImages()) {
@ -190,6 +191,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.createAdminSettings();
systemDataLoaderService.createRandomJwtSettings();
systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.loadSystemScadaSymbols();
systemDataLoaderService.createOAuth2Templates();
systemDataLoaderService.createQueues();
systemDataLoaderService.createDefaultNotificationConfigs();

5
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -526,6 +526,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
installScripts.loadSystemWidgets();
}
@Override
public void loadSystemScadaSymbols() throws Exception {
installScripts.loadSystemScadaSymbols();
}
private User createUser(Authority authority,
TenantId tenantId,
CustomerId customerId,

96
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
@ -26,8 +27,12 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@ -43,8 +48,10 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.util.ImageUtils;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater;
@ -84,12 +91,14 @@ public class InstallScripts {
public static final String RULE_CHAINS_DIR = "rule_chains";
public static final String WIDGET_TYPES_DIR = "widget_types";
public static final String WIDGET_BUNDLES_DIR = "widget_bundles";
public static final String SCADA_SYMBOLS_DIR = "scada_symbols";
public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
public static final String DASHBOARDS_DIR = "dashboards";
public static final String MODELS_LWM2M_DIR = "lwm2m-registry";
public static final String CREDENTIALS_DIR = "credentials";
public static final String JSON_EXT = ".json";
public static final String SVG_EXT = ".svg";
public static final String XML_EXT = ".xml";
@Value("${install.data_dir:}")
@ -118,6 +127,9 @@ public class InstallScripts {
@Getter @Setter
private boolean updateImages = false;
@Autowired
private ImageService imageService;
Path getTenantRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
}
@ -290,6 +302,90 @@ public class InstallScripts {
}
}
public void loadSystemScadaSymbols() throws Exception {
log.info("Loading system SCADA symbols");
Path scadaSymbolsDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, SCADA_SYMBOLS_DIR);
if (Files.exists(scadaSymbolsDir)) {
WidgetTypeDetails widgetTypeDetailsTemplate = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "scada_symbol");
List<String> widgetTypeFqns = new ArrayList<>();
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(scadaSymbolsDir, path -> path.toString().endsWith(SVG_EXT))) {
dirStream.forEach(
path -> {
try {
var fileName = path.getFileName().toString();
var scadaSymbolData = Files.readAllBytes(path);
var metadata = ImageUtils.processScadaSymbolMetadata(fileName, scadaSymbolData);
TbResourceInfo savedScadaSymbol = saveScadaSymbol(metadata, fileName, scadaSymbolData);
if (widgetTypeDetailsTemplate != null) {
widgetTypeFqns.add(saveScadaSymbolWidget(widgetTypeDetailsTemplate, savedScadaSymbol, metadata));
}
} catch (Exception e) {
log.error("Unable to load SCADA symbol from file: [{}]", path.toString());
throw new RuntimeException("Unable to load SCADA symbol from file", e);
}
}
);
}
var scadaSymbolsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "scada_symbols");
if (scadaSymbolsBundle != null) {
widgetTypeFqns.add("scada_symbol");
widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, scadaSymbolsBundle.getId(), widgetTypeFqns);
}
}
}
private TbResourceInfo saveScadaSymbol(ImageUtils.ScadaSymbolMetadataInfo metadata, String fileName, byte[] scadaSymbolData) {
String etag = imageService.calculateImageEtag(scadaSymbolData);
var existingImage = imageService.findSystemOrTenantImageByEtag(TenantId.SYS_TENANT_ID, etag);
if (existingImage != null && ResourceSubType.SCADA_SYMBOL.equals(existingImage.getResourceSubType())) {
return existingImage;
} else {
TbResource image = new TbResource();
image.setTenantId(TenantId.SYS_TENANT_ID);
image.setFileName(fileName);
image.setTitle(metadata.getTitle());
image.setResourceSubType(ResourceSubType.SCADA_SYMBOL);
image.setResourceType(ResourceType.IMAGE);
image.setPublic(true);
ImageDescriptor descriptor = new ImageDescriptor();
descriptor.setMediaType("image/svg+xml");
image.setDescriptorValue(descriptor);
image.setData(scadaSymbolData);
return imageService.saveImage(image);
}
}
private String saveScadaSymbolWidget(WidgetTypeDetails template, TbResourceInfo scadaSymbol,
ImageUtils.ScadaSymbolMetadataInfo metadata) {
String symbolUrl = DataConstants.TB_IMAGE_PREFIX + scadaSymbol.getLink();
WidgetTypeDetails scadaSymbolWidget = new WidgetTypeDetails();
JsonNode descriptor = JacksonUtil.clone(template.getDescriptor());
scadaSymbolWidget.setDescriptor(descriptor);
scadaSymbolWidget.setName(metadata.getTitle());
scadaSymbolWidget.setImage(symbolUrl);
scadaSymbolWidget.setDescription(metadata.getDescription());
scadaSymbolWidget.setTags(metadata.getSearchTags());
ObjectNode defaultConfig = null;
if (descriptor.has("defaultConfig")) {
defaultConfig = JacksonUtil.fromString(descriptor.get("defaultConfig").asText(), ObjectNode.class);
}
if (defaultConfig == null) {
defaultConfig = JacksonUtil.newObjectNode();
}
defaultConfig.put("title", metadata.getTitle());
ObjectNode settings;
if (defaultConfig.has("settings")) {
settings = (ObjectNode)defaultConfig.get("settings");
} else {
settings = JacksonUtil.newObjectNode();
defaultConfig.set("settings", settings);
}
settings.put("scadaSymbolUrl", symbolUrl);
((ObjectNode)descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig));
var savedWidget = widgetTypeService.saveWidgetType(scadaSymbolWidget);
return savedWidget.getFqn();
}
private void deleteSystemWidgetBundle(String bundleAlias) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) {

2
application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java

@ -31,6 +31,8 @@ public interface SystemDataLoaderService {
void loadSystemWidgets() throws Exception;
void loadSystemScadaSymbols() throws Exception;
void loadDemoData() throws Exception;
void createQueues();

2
common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java

@ -47,6 +47,8 @@ public interface ImageService {
TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force);
String calculateImageEtag(byte[] imageData);
TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag);
boolean replaceBase64WithImageUrl(HasImage entity, String type);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java

@ -56,6 +56,8 @@ public interface WidgetTypeService extends EntityDaoService {
WidgetType findWidgetTypeByTenantIdAndFqn(TenantId tenantId, String fqn);
WidgetTypeDetails findWidgetTypeDetailsByTenantIdAndFqn(TenantId tenantId, String fqn);
void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List<WidgetTypeId> widgetTypeIds);
void updateWidgetsBundleWidgetFqns(TenantId tenantId, WidgetsBundleId widgetsBundleId, List<String> widgetFqns);

5
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -287,6 +287,11 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return result.success(success).build();
}
@Override
public String calculateImageEtag(byte[] imageData) {
return calculateEtag(imageData);
}
@Override
public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) {
log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, etag);

5
dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java

@ -176,6 +176,11 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndFqn(tenantId, fqn));
}
@Override
public WidgetTypeDetails findDetailsByTenantIdAndFqn(UUID tenantId, String fqn) {
return DaoUtil.getData(widgetTypeRepository.findByTenantIdAndFqn(tenantId, fqn));
}
@Override
public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndResourceId(tenantId, tbResourceId));

2
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java

@ -67,6 +67,8 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
WidgetTypeEntity findWidgetTypeByTenantIdAndFqn(@Param("tenantId") UUID tenantId,
@Param("fqn") String fqn);
WidgetTypeDetailsEntity findByTenantIdAndFqn(UUID tenantId, String fqn);
@Query(value = "SELECT * FROM widget_type wt " +
"WHERE wt.tenant_id = :tenantId AND cast(wt.descriptor as json) ->> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))",
nativeQuery = true)

43
dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java

@ -19,6 +19,8 @@ import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
@ -39,6 +41,7 @@ import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.w3c.dom.Document;
@ -234,6 +237,17 @@ public class ImageUtils {
return originalImage;
}
public static ScadaSymbolMetadataInfo processScadaSymbolMetadata(String fileName, byte[] data) throws Exception {
SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
Document document = factory.createDocument(null, new ByteArrayInputStream(data));
var metaElements = document.getElementsByTagName("tb:metadata");
JsonNode metaData = null;
if (metaElements.getLength() > 0) {
metaData = JacksonUtil.toJsonNode(metaElements.item(0).getTextContent());
}
return new ScadaSymbolMetadataInfo(fileName, metaData);
}
private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension, boolean originalIfSmaller) {
if (originalWidth <= maxDimension && originalHeight <= maxDimension && originalIfSmaller) {
return new int[]{originalWidth, originalHeight};
@ -264,4 +278,33 @@ public class ImageUtils {
private ProcessedImage preview;
}
@Data
public static class ScadaSymbolMetadataInfo {
private String title;
private String description;
private String[] searchTags;
public ScadaSymbolMetadataInfo(String fileName, JsonNode metaData) {
if (metaData != null && metaData.has("title")) {
title = metaData.get("title").asText();
} else {
title = fileName;
}
if (metaData != null && metaData.has("description")) {
description = metaData.get("description").asText();
} else {
description = "";
}
if (metaData != null && metaData.has("searchTags") && metaData.get("searchTags").isArray()) {
var tagsNode = (ArrayNode) metaData.get("searchTags");
searchTags = new String[tagsNode.size()];
for (int i = 0; i < tagsNode.size(); i++) {
searchTags[i] = tagsNode.get(i).asText();
}
} else {
searchTags = new String[0];
}
}
}
}

2
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java

@ -92,6 +92,8 @@ public interface WidgetTypeDao extends Dao<WidgetTypeDetails>, ExportableEntityD
*/
WidgetType findByTenantIdAndFqn(UUID tenantId, String fqn);
WidgetTypeDetails findDetailsByTenantIdAndFqn(UUID tenantId, String fqn);
/**
* Find widget types infos by tenantId and resourceId in descriptor.
*

9
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -189,6 +189,15 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return widgetTypeDao.findByTenantIdAndFqn(tenantId.getId(), fqn);
}
@Override
public WidgetTypeDetails findWidgetTypeDetailsByTenantIdAndFqn(TenantId tenantId, String fqn) {
log.trace("Executing findWidgetTypeDetailsByTenantIdAndFqn, tenantId [{}], fqn [{}]", tenantId, fqn);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
Validator.validateString(fqn, f -> "Incorrect fqn " + f);
return widgetTypeDao.findDetailsByTenantIdAndFqn(tenantId.getId(), fqn);
}
@Override
public void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List<WidgetTypeId> widgetTypeIds) {
log.trace("Executing updateWidgetsBundleWidgetTypes, tenantId [{}], widgetsBundleId [{}], widgetTypeIds [{}]", tenantId, widgetsBundleId, widgetTypeIds);

8
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts

@ -33,12 +33,13 @@ import {
ComparisonResultType,
DataKey,
DataKeyConfigMode,
DatasourceType, JsonSettingsSchema, Widget,
DatasourceType,
JsonSettingsSchema,
Widget,
widgetType
} from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { TranslateService } from '@ngx-translate/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import {
@ -51,6 +52,7 @@ import {
aggregatedValueCardKeyPositionTranslations,
AggregatedValueCardKeySettings
} from '@home/components/widget/lib/cards/aggregated-value-card.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
@Component({
selector: 'tb-aggregated-data-key-row',
@ -99,7 +101,7 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn
valuePreviewFn = this._valuePreviewFn.bind(this);
get callbacks(): DataKeysCallbacks {
get callbacks(): WidgetConfigCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}

14
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -19,9 +19,11 @@ import {
Component,
EventEmitter,
forwardRef,
Input, OnChanges,
Input,
OnChanges,
OnInit,
Output, SimpleChanges,
Output,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import {
@ -45,7 +47,7 @@ import {
widgetType
} from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { merge } from 'rxjs';
import {
DataKeyConfigDialogComponent,
@ -60,8 +62,10 @@ import {
TimeSeriesChartSeriesType,
timeSeriesChartSeriesTypeIcons,
timeSeriesChartSeriesTypes,
timeSeriesChartSeriesTypeTranslations, TimeSeriesChartYAxisId
timeSeriesChartSeriesTypeTranslations,
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
export const dataKeyValid = (key: DataKey): boolean => !!key && !!key.type && !!key.name;
@ -177,7 +181,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.widgetType;
}
get callbacks(): DataKeysCallbacks {
get callbacks(): WidgetConfigCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}

4
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts

@ -30,12 +30,12 @@ import {
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { DataKey, DataKeyConfigMode, Widget, widgetType } from '@shared/models/widget.models';
import { DataKeysCallbacks } from './data-keys.component.models';
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { TranslateService } from '@ngx-translate/core';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
export interface DataKeyConfigDialogData {
dataKey: DataKey;
@ -49,7 +49,7 @@ export interface DataKeyConfigDialogData {
deviceId?: string;
entityAliasId?: string;
showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks;
callbacks?: WidgetConfigCallbacks;
hideDataKeyName?: boolean;
hideDataKeyLabel?: boolean;
hideDataKeyColor?: boolean;

2
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html

@ -196,7 +196,7 @@
<tb-widget-settings
[dashboard]="dashboard"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[callbacks]="callbacks"
[widget]="widget"
[widgetConfig]="widgetConfig"
formControlName="settings">

12
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts

@ -22,17 +22,18 @@ import {
ComparisonResultType,
comparisonResultTypeTranslationMap,
DataKey,
dataKeyAggregationTypeHintTranslationMap, DataKeyConfigMode,
dataKeyAggregationTypeHintTranslationMap,
DataKeyConfigMode,
Widget,
widgetType
} from '@shared/models/widget.models';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
@ -40,7 +41,7 @@ import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable, of } from 'rxjs';
import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators';
@ -55,6 +56,7 @@ import { genNextLabel, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
@Component({
selector: 'tb-data-key-config',
@ -105,7 +107,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
entityAliasId: string;
@Input()
callbacks: DataKeysCallbacks;
callbacks: WidgetConfigCallbacks;
@Input()
dashboard: Dashboard;

11
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts

@ -33,13 +33,15 @@ import {
import {
AbstractControl,
ControlValueAccessor,
FormGroupDirective, NG_VALIDATORS,
FormGroupDirective,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors, Validator
ValidationErrors,
Validator
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
@ -52,7 +54,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from './data-keys.component.models';
import { DataKeySettingsFunction } from './data-keys.component.models';
import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import { ErrorStateMatcher } from '@angular/material/core';
@ -71,6 +73,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
import { DatasourceComponent } from '@home/components/widget/config/datasource.component';
import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component';
import { TbPopoverService } from '@shared/components/popover.service';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
@Component({
selector: 'tb-data-keys',
@ -166,7 +169,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
widget: Widget;
@Input()
callbacks: DataKeysCallbacks;
callbacks: WidgetConfigCallbacks;
@Input()
entityAliasId: string;

4
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html

@ -75,7 +75,7 @@
[datakeySettingsFunction]="dataKeySettingsFunction"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="dataKeysCallbacks"
[callbacks]="callbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="dataKeys">
@ -91,7 +91,7 @@
[datakeySettingsFunction]="dataKeySettingsFunction"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="dataKeysCallbacks"
[callbacks]="callbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="latestDataKeys">

5
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts

@ -42,6 +42,7 @@ import { FilterSelectCallbacks } from '@home/components/filter/filter-select.com
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DatasourcesComponent } from '@home/components/widget/config/datasources.component';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
@Component({
selector: 'tb-datasource',
@ -82,6 +83,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.widgetConfigComponent.widgetConfigCallbacks;
}
public get callbacks(): WidgetConfigCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
public get dataKeysCallbacks(): DataKeysCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}

11
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts

@ -50,6 +50,7 @@ import { WidgetService } from '@core/http/widget.service';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
@Component({
selector: 'tb-widget-settings',
@ -79,7 +80,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
aliasController: IAliasController;
@Input()
dataKeyCallbacks: DataKeysCallbacks;
callbacks: WidgetConfigCallbacks;
@Input()
dashboard: Dashboard;
@ -142,9 +143,10 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
this.definedSettingsComponent.aliasController = this.aliasController;
}
}
if (propName === 'dataKeyCallbacks') {
if (propName === 'callbacks') {
if (this.definedSettingsComponent) {
this.definedSettingsComponent.dataKeyCallbacks = this.dataKeyCallbacks;
this.definedSettingsComponent.callbacks = this.callbacks;
this.definedSettingsComponent.dataKeyCallbacks = this.callbacks;
}
}
if (propName === 'widgetConfig') {
@ -234,7 +236,8 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory);
this.definedSettingsComponent = this.definedSettingsComponentRef.instance;
this.definedSettingsComponent.aliasController = this.aliasController;
this.definedSettingsComponent.dataKeyCallbacks = this.dataKeyCallbacks;
this.definedSettingsComponent.callbacks = this.callbacks;
this.definedSettingsComponent.dataKeyCallbacks = this.callbacks;
this.definedSettingsComponent.dashboard = this.dashboard;
this.definedSettingsComponent.widget = this.widget;
this.definedSettingsComponent.widgetConfig = this.widgetConfig;

3
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html

@ -15,8 +15,7 @@
limitations under the License.
-->
<div class="tb-scada-symbol-panel" [style.padding]="padding"
[class.overlay]="overlayEnabled" [style]="backgroundStyle$ | async">
<div class="tb-scada-symbol-panel" [style.padding]="padding" [style]="backgroundStyle$ | async">
<div class="tb-scada-symbol-overlay" [style]="overlayStyle"></div>
<ng-container *ngIf="widgetComponent.dashboardWidget.showWidgetTitlePanel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>

4
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.scss

@ -21,9 +21,7 @@
display: flex;
flex-direction: column;
gap: 8px;
&.overlay {
padding: 20px 24px 24px 24px;
}
padding: 20px 24px 24px 24px;
> div:not(.tb-scada-symbol-overlay) {
z-index: 1;
}

4
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts

@ -64,7 +64,6 @@ export class ScadaSymbolWidgetComponent implements OnInit, AfterViewInit, OnDest
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
overlayEnabled: boolean;
padding: string;
loading$ = this.loadingSubject.asObservable().pipe(share());
@ -84,8 +83,7 @@ export class ScadaSymbolWidgetComponent implements OnInit, AfterViewInit, OnDest
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.overlayEnabled = this.settings.background.overlay.enabled;
this.padding = this.overlayEnabled ? undefined : this.settings.padding;
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
if (this.settings.scadaSymbolContent) {
this.scadaSymbolContent$ = of(this.settings.scadaSymbolContent);

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

@ -175,6 +175,8 @@ export type ScadaSymbolProperty = ScadaSymbolPropertyBase & ScadaSymbolNumberPro
export interface ScadaSymbolMetadata {
title: string;
description?: string;
searchTags?: string[];
stateRenderFunction?: string;
stateRender?: ScadaSymbolStateRenderFunction;
tags: ScadaSymbolTag[];

46
ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.html

@ -0,0 +1,46 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="scadaSymbolWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>scada.symbol</div>
<tb-scada-symbol-input required
formControlName="scadaSymbolUrl"></tb-scada-symbol-input>
</div>
<tb-scada-symbol-object-settings
formControlName="scadaSymbolObjectSettings"
[scadaSymbolUrl]="scadaSymbolWidgetSettingsForm.get('scadaSymbolUrl').value"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
[callbacks]="callbacks">
</tb-scada-symbol-object-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</ng-container>

62
ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.ts

@ -0,0 +1,62 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { scadaSymbolWidgetDefaultSettings } from '@home/components/widget/lib/scada/scada-symbol-widget.models';
@Component({
selector: 'tb-scada-symbol-widget-settings',
templateUrl: './scada-symbol-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class ScadaSymbolWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
return this.widgetConfig?.config?.targetDevice;
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
scadaSymbolWidgetSettingsForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.scadaSymbolWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...scadaSymbolWidgetDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.scadaSymbolWidgetSettingsForm = this.fb.group({
scadaSymbolUrl: [settings.scadaSymbolUrl, []],
scadaSymbolObjectSettings: [settings.scadaSymbolObjectSettings, []],
background: [settings.background, []],
padding: [settings.padding, []]
});
}
}

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -356,6 +356,9 @@ import {
import {
MobileAppQrCodeWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component';
import {
ScadaSymbolWidgetSettingsComponent
} from '@home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component';
@NgModule({
declarations: [
@ -482,7 +485,8 @@ import {
PieChartWidgetSettingsComponent,
BarChartWidgetSettingsComponent,
PolarAreaChartWidgetSettingsComponent,
RadarChartWidgetSettingsComponent
RadarChartWidgetSettingsComponent,
ScadaSymbolWidgetSettingsComponent
],
imports: [
CommonModule,
@ -614,7 +618,8 @@ import {
PieChartWidgetSettingsComponent,
BarChartWidgetSettingsComponent,
PolarAreaChartWidgetSettingsComponent,
RadarChartWidgetSettingsComponent
RadarChartWidgetSettingsComponent,
ScadaSymbolWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -713,5 +718,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-pie-chart-widget-settings': PieChartWidgetSettingsComponent,
'tb-bar-chart-widget-settings': BarChartWidgetSettingsComponent,
'tb-polar-area-chart-widget-settings': PolarAreaChartWidgetSettingsComponent,
'tb-radar-chart-widget-settings': RadarChartWidgetSettingsComponent
'tb-radar-chart-widget-settings': RadarChartWidgetSettingsComponent,
'tb-scada-symbol-widget-settings': ScadaSymbolWidgetSettingsComponent
};

2
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -288,7 +288,7 @@
<div *ngIf="displayAdvancedAppearance" style="height: 100%;" [formGroup]="advancedSettings">
<tb-widget-settings
[aliasController]="aliasController"
[dataKeyCallbacks]="widgetConfigCallbacks"
[callbacks]="widgetConfigCallbacks"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfig]="modelValue"

19
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html

@ -33,6 +33,25 @@
<input matInput required formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.description</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<textarea matInput
formControlName="description"
rows="2"></textarea>
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.search-tags</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<tb-string-items-list fxFlex
style="display: flex;"
formControlName="searchTags"
appearance="outline"
subscriptSizing="dynamic">
</tb-string-items-list>
</div>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header fxLayout="row wrap">

12
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts

@ -128,6 +128,8 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni
ngOnInit(): void {
this.metadataFormGroup = this.fb.group({
title: [null, [Validators.required]],
description: [null],
searchTags: [null],
stateRenderFunction: [null],
tags: [null],
behavior: [null],
@ -170,7 +172,15 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni
writeValue(value: ScadaSymbolMetadata): void {
this.modelValue = value;
this.metadataFormGroup.patchValue(
value, {emitEvent: false}
{
title: value?.title,
description: value?.description,
searchTags: value?.searchTags,
stateRenderFunction: value?.stateRenderFunction,
tags: value?.tags,
behavior: value?.behavior,
properties: value?.properties
}, {emitEvent: false}
);
this.updateFunctionCompleters(value);
}

37
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html

@ -16,3 +16,40 @@
-->
<div class="tb-scada-symbol-editor-shape" #scadaSymbolShape></div>
<div class="tb-scada-symbol-editor-tooltips" #tooltipsContainer>
<tb-anchor #tooltipsContainerComponent></tb-anchor>
</div>
<div class="tb-scada-symbol-editor-buttons">
<div class="tb-scada-symbol-editor-zoom-buttons tb-primary-fill">
<button mat-icon-button
[disabled]="zoomInDisabled"
(click)="zoomIn()"
matTooltip="{{ 'scada.zoom-in' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-icon-button
[disabled]="zoomOutDisabled"
(click)="zoomOut()"
matTooltip="{{ 'scada.zoom-out' | translate }}"
matTooltipPosition="above">
<mat-icon>remove</mat-icon>
</button>
</div>
<div class="tb-scada-symbol-editor-upload-buttons">
<div class="tb-primary-fill">
<button mat-icon-button
matTooltip="{{ 'scada.update-symbol' | translate }}"
matTooltipPosition="above">
<mat-icon>file_upload</mat-icon>
</button>
</div>
<div class="tb-primary-fill">
<button mat-icon-button
matTooltip="{{ 'scada.download-symbol' | translate }}"
matTooltipPosition="above">
<mat-icon>download</mat-icon>
</button>
</div>
</div>
</div>

193
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss

@ -20,3 +20,196 @@
align-items: center;
justify-content: center;
}
.tb-scada-symbol-editor-tooltips {
position: absolute;
inset: 0;
pointer-events: none;
}
.tb-scada-symbol-editor-buttons {
pointer-events: none;
position: absolute;
left: 20px;
top: 20px;
right: 20px;
display: flex;
justify-content: space-between;
align-items: flex-start;
.tb-scada-symbol-editor-zoom-buttons {
display: flex;
flex-direction: column;
z-index: 101;
background: #fff;
border-radius: 24px;
&.tb-primary-fill {
&:before {
opacity: 0.1;
}
}
}
.tb-scada-symbol-editor-upload-buttons {
display: flex;
flex-direction: row;
gap: 8px;
z-index: 101;
.tb-primary-fill {
background: #fff;
border-radius: 50%;
&:before {
opacity: 0.1;
}
}
}
}
.tooltipster-sidetip.scada-symbol {
position: fixed;
.tooltipster-box {
user-select: none;
background: rgba(0, 0, 0, 0.54);
border: 1px solid rgba(0, 0, 0, 0);
border-radius: 8px;
display: flex;
align-items: center;
.tooltipster-content {
padding: 4px 8px;
font-size: 12px;
line-height: 12px;
font-weight: 500;
color: #ffffff;
}
}
&.tb-active {
.tooltipster-box {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.38);
box-shadow: 0 0 10px 6px rgba(0, 0, 0, .2);
.tooltipster-content {
color: rgba(0, 0, 0, 0.76);
}
}
.tooltipster-arrow {
.tooltipster-arrow-uncropped {
.tooltipster-arrow-background {
border-width: 12px;
}
}
}
&.tooltipster-top {
.tooltipster-arrow {
bottom: -1px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-top-color: rgba(0, 0, 0, 0.38);
}
.tooltipster-arrow-background {
border-top-color: #fff;
left: -2px;
}
}
}
}
&.tooltipster-bottom {
.tooltipster-arrow {
top: -1px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-bottom-color: rgba(0, 0, 0, 0.38);
}
.tooltipster-arrow-background {
border-bottom-color: #fff;
left: -2px;
top: -1px;
}
}
}
}
&.tooltipster-left {
.tooltipster-arrow {
right: -1px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-left-color: rgba(0, 0, 0, 0.38);
}
.tooltipster-arrow-background {
border-left-color: #fff;
top: -2px;
}
}
}
}
&.tooltipster-right {
.tooltipster-arrow {
left: -1px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-right-color: rgba(0, 0, 0, 0.38);
}
.tooltipster-arrow-background {
border-right-color: #fff;
top: -2px;
left: -1px;
}
}
}
}
}
&.tooltipster-left, &.tooltipster-right {
.tooltipster-box {
min-height: 36px;
}
}
&.tooltipster-top {
.tooltipster-arrow {
bottom: -2px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-top-color: rgba(0, 0, 0, 0.54);
}
.tooltipster-arrow-background {
border-top-color: transparent;
}
}
}
}
&.tooltipster-bottom {
.tooltipster-arrow {
top: -2px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-bottom-color: rgba(0, 0, 0, 0.54);
}
.tooltipster-arrow-background {
border-bottom-color: transparent;
}
}
}
}
&.tooltipster-left {
.tooltipster-arrow {
right: -2px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-left-color: rgba(0, 0, 0, 0.54);
}
.tooltipster-arrow-background {
border-left-color: transparent;
}
}
}
}
&.tooltipster-right {
.tooltipster-arrow {
left: -2px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-right-color: rgba(0, 0, 0, 0.54);
}
.tooltipster-arrow-background {
border-right-color: transparent;
}
}
}
}
}

45
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts

@ -15,7 +15,7 @@
///
import {
AfterViewInit,
AfterViewInit, ChangeDetectorRef,
Component,
ElementRef,
Input,
@ -28,6 +28,7 @@ import {
ViewEncapsulation
} from '@angular/core';
import { ScadaSymbolEditObject, ScadaSymbolEditObjectCallbacks } from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
export interface ScadaSymbolEditorData {
scadaSymbolContent: string;
@ -44,6 +45,12 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
@ViewChild('scadaSymbolShape', {static: false})
scadaSymbolShape: ElementRef<HTMLElement>;
@ViewChild('tooltipsContainer', {static: false})
tooltipsContainer: ElementRef<HTMLElement>;
@ViewChild('tooltipsContainerComponent', {static: true})
tooltipsContainerComponent: TbAnchorComponent;
@Input()
data: ScadaSymbolEditorData;
@ -55,17 +62,24 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
scadaSymbolEditObject: ScadaSymbolEditObject;
constructor(private viewContainerRef: ViewContainerRef) {
zoomInDisabled = false;
zoomOutDisabled = false;
constructor(private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
}
ngAfterViewInit() {
this.editObjectCallbacks.onZoom = () => {
this.updateZoomButtonsState();
};
this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.scadaSymbolShape.nativeElement,
this.viewContainerRef, this.editObjectCallbacks, this.readonly);
this.tooltipsContainer.nativeElement,
this.tooltipsContainerComponent.viewContainerRef, this.editObjectCallbacks, this.readonly);
if (this.data) {
this.scadaSymbolEditObject.setContent(this.data.scadaSymbolContent);
this.updateContent(this.data.scadaSymbolContent);
}
}
@ -76,7 +90,7 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
if (propName === 'data') {
if (this.scadaSymbolEditObject) {
setTimeout(() => {
this.scadaSymbolEditObject.setContent(this.data.scadaSymbolContent);
this.updateContent(this.data.scadaSymbolContent);
});
}
} else if (propName === 'readonly') {
@ -93,5 +107,26 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
getContent(): string {
return this.scadaSymbolEditObject?.getContent();
}
zoomIn() {
this.scadaSymbolEditObject.zoomIn();
}
zoomOut() {
this.scadaSymbolEditObject.zoomOut();
}
private updateContent(content: string) {
this.scadaSymbolEditObject.setContent(content);
setTimeout(() => {
this.updateZoomButtonsState();
});
}
private updateZoomButtonsState() {
this.zoomInDisabled = this.scadaSymbolEditObject.zoomInDisabled();
this.zoomOutDisabled = this.scadaSymbolEditObject.zoomOutDisabled();
this.cd.markForCheck();
}
}

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

@ -53,8 +53,12 @@ export interface ScadaSymbolEditObjectCallbacks {
editTagClickAction: (tag: string) => void;
tagsUpdated: (tags: string[]) => void;
onSymbolEditObjectDirty: (dirty: boolean) => void;
onZoom?: () => void;
}
const minSymbolZoom = 1;
const maxSymbolZoom = 4;
export class ScadaSymbolEditObject {
public svgShape: Svg;
@ -68,7 +72,10 @@ export class ScadaSymbolEditObject {
public tags: string[] = [];
private zoomFactor = 0.34;
constructor(private rootElement: HTMLElement,
public tooltipsContainer: HTMLElement,
public viewContainerRef: ViewContainerRef,
private callbacks: ScadaSymbolEditObjectCallbacks,
public readonly: boolean) {
@ -126,6 +133,40 @@ export class ScadaSymbolEditObject {
this.elements.filter(e => e.isEditing()).forEach(e => e.stopEdit(true));
}
public zoomIn() {
const level = Math.min(Math.pow(1 + this.zoomFactor, 1.2) * this.svgShape.zoom(), maxSymbolZoom);
this.zoomAnimate(level);
}
public zoomOut() {
const level = Math.max(Math.pow(1 + this.zoomFactor, -1.2) * this.svgShape.zoom(), minSymbolZoom);
this.zoomAnimate(level);
}
private zoomAnimate(level: number, animationMs = 200) {
if (level !== this.svgShape.zoom()) {
this.hideTooltips();
const runner = this.svgShape.animate(animationMs).ease('<>');
runner
.zoom(level)
.during(() => {
const box = this.restrictToMargins(this.svgShape.viewbox(), 0);
this.svgShape.viewbox(box);
})
.after(() => {
this.postZoom(this.callbacks.onZoom);
});
}
}
public zoomInDisabled() {
return Number(this.svgShape.zoom().toFixed(5)) >= maxSymbolZoom;
}
public zoomOutDisabled() {
return Number(this.svgShape.zoom().toFixed(5)) <= minSymbolZoom;
}
private doSetup() {
this.setupZoomPan(0);
(window as any).SVG = svgjs;
@ -143,29 +184,37 @@ export class ScadaSymbolEditObject {
detail: { level, focus }
} = e as any;
this.svgShape.zoom(level, focus);
const box = this.restrictToMargins(this.svgShape.viewbox(), margin);
this.svgShape.viewbox(box);
setTimeout(() => {
this.updateTooltipPositions();
});
e.preventDefault();
this.postZoom(this.callbacks.onZoom, e, margin);
});
this.svgShape.on('panning', (e) => {
const box = (e as any).detail.box;
this.svgShape.viewbox(this.restrictToMargins(box, margin));
setTimeout(() => {
this.updateTooltipPositions();
});
e.preventDefault();
});
this.svgShape.on('panStart', (_e) => {
this.svgShape.node.style.cursor = 'grab';
if (this.svgShape.zoom() > minSymbolZoom) {
this.hideTooltips();
this.svgShape.node.style.cursor = 'grab';
}
});
this.svgShape.on('panEnd', (_e) => {
this.restoreTooltips();
this.svgShape.node.style.cursor = 'default';
});
}
private postZoom(callback?: () => void, e?: any, margin = 0) {
const box = this.restrictToMargins(this.svgShape.viewbox(), margin);
this.svgShape.viewbox(box);
setTimeout(() => {
this.updateTooltipPositions();
});
e?.preventDefault();
if (callback) {
callback();
}
}
private restrictToMargins(box: Box, margin: number): Box {
if (box.x < -margin) {
box.x = -margin;
@ -304,17 +353,29 @@ export class ScadaSymbolEditObject {
}
private updateZoomOptions() {
// this.zoomFactor = 2 / this.scale;
this.svgShape.panZoom({
zoomMin: 1,
zoomMax: 4,
zoomFactor: 2 / this.scale
zoomMin: minSymbolZoom,
zoomMax: maxSymbolZoom,
zoomFactor: this.zoomFactor
});
}
private updateTooltipPositions() {
const container = this.rootElement.getBoundingClientRect();
for (const e of this.elements) {
e.updateTooltipPosition(container);
e.updateTooltipPosition();
}
}
private hideTooltips() {
for (const e of this.elements) {
e.hideTooltip();
}
}
private restoreTooltips() {
for (const e of this.elements) {
e.restoreTooltip();
}
}
@ -362,12 +423,12 @@ const hasBBox = (e: Element): boolean => {
}
};
const isDomRectContained = (target: DOMRect, container: DOMRect, horizontalGap = 0, verticalGap = 0): boolean => (
/*const isDomRectContained = (target: DOMRect, container: DOMRect, horizontalGap = 0, verticalGap = 0): boolean => (
target.left >= container.left - horizontalGap &&
target.left + target.width <= container.left + container.width + horizontalGap &&
target.top >= container.top - verticalGap &&
target.top + target.height <= container.top + container.height + verticalGap
);
);*/
const elementTooltipMinHeight = 36 + 8;
const elementTooltipMinWidth = 100;
@ -559,21 +620,29 @@ export class ScadaSymbolElement {
return this.editing;
}
public updateTooltipPosition(container: DOMRect) {
public updateTooltipPosition() {
if (this.tooltip && !this.tooltip.status().destroyed) {
this.tooltip.reposition();
const tooltipElement = this.tooltip.elementTooltip();
if (tooltipElement) {
if (isDomRectContained(tooltipElement.getBoundingClientRect(), container,
elementTooltipMinWidth, elementTooltipMinHeight)) {
tooltipElement.style.visibility = null;
} else {
tooltipElement.style.visibility = 'hidden';
}
tooltipElement.style.visibility = null;
}
}
}
public hideTooltip() {
if (this.tooltip && !this.tooltip.status().destroyed) {
const tooltipElement = this.tooltip.elementTooltip();
if (tooltipElement) {
tooltipElement.style.visibility = 'hidden';
}
}
}
public restoreTooltip() {
this.updateTooltipPosition();
}
public setInnerTooltipOffset(offset: number, center: number) {
this.innerTooltipOffset = offset + (center - this.box.cy) * this.editObject.scale;
}
@ -584,6 +653,10 @@ export class ScadaSymbolElement {
}
}
public get tooltipContainer(): JQuery<HTMLElement> {
return $(this.editObject.tooltipsContainer);
}
private unscaled(size: number): number {
return size / (this.editObject.scale * this.editObject.svgShape.zoom());
}
@ -596,6 +669,7 @@ export class ScadaSymbolElement {
const el = $(this.element.node);
el.tooltipster(
{
parent: this.tooltipContainer,
zIndex: 100,
arrow: this.isGroup(),
distance: this.isGroup() ? (this.scaled(groupRectPadding) + groupRectStroke) : 6,
@ -611,6 +685,7 @@ export class ScadaSymbolElement {
this.innerTagTooltipPosition(instance, helper, position),
functionReady: (_instance, helper) => {
const tooltipEl = $(helper.tooltip);
// tooltipEl.detach().appendTo($(this.editObject.tooltipsContainer));
tooltipEl.on('mouseenter', () => {
this.highlight();
});
@ -632,6 +707,7 @@ export class ScadaSymbolElement {
const el = $(this.element.node);
el.tooltipster(
{
parent: this.tooltipContainer,
zIndex: 100,
arrow: true,
theme: ['scada-symbol', 'tb-active'],

2
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts

@ -244,6 +244,7 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements
const tagSettingsButton = $(this.tagSettingsButton.nativeElement);
tagSettingsButton.tooltipster(
{
parent: this.symbolElement.tooltipContainer,
zIndex: 200,
arrow: true,
theme: ['scada-symbol', 'tb-active'],
@ -264,6 +265,7 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements
const removeTagButton = $(this.removeTagButton.nativeElement);
removeTagButton.tooltipster(
{
parent: this.symbolElement.tooltipContainer,
zIndex: 200,
arrow: true,
theme: ['scada-symbol', 'tb-active'],

4
ui-ngx/src/app/shared/models/widget.models.ts

@ -43,6 +43,7 @@ import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-set
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { HasTenantId } from '@shared/models/entity.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
export enum widgetType {
timeseries = 'timeseries',
@ -833,6 +834,7 @@ export interface WidgetSize {
export interface IWidgetSettingsComponent {
aliasController: IAliasController;
callbacks: WidgetConfigCallbacks;
dataKeyCallbacks: DataKeysCallbacks;
dashboard: Dashboard;
widget: Widget;
@ -851,6 +853,8 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
aliasController: IAliasController;
callbacks: WidgetConfigCallbacks;
dataKeyCallbacks: DataKeysCallbacks;
dashboard: Dashboard;

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

@ -3432,6 +3432,7 @@
"export-symbol": "Export SCADA symbol to JSON",
"import-symbol": "Import SCADA symbol from JSON",
"upload-symbol": "Upload SCADA symbol",
"update-symbol": "Update SCADA symbol",
"edit-symbol": "Edit SCADA symbol",
"symbol-details": "SCADA symbol details",
"no-symbols": "No symbols found",
@ -3446,6 +3447,8 @@
"tags": "Tags",
"properties": "Properties",
"title": "Title",
"description": "Description",
"search-tags": "Search tags",
"state-render-function": "State render function",
"preview": "Preview",
"preview-widget-action-text": "Widget action '{{type}}' successfully invoked!",
@ -3453,6 +3456,8 @@
"no-symbol-selected": "No SCADA symbol selected",
"clear-symbol": "Clear SCADA symbol",
"browse-symbol-from-gallery": "Browse SCADA symbol from gallery",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"tag": {
"tag": "Tag",
"on-click-action": "On click action",

150
ui-ngx/src/styles.scss

@ -461,156 +461,6 @@ mat-icon {
}
}
.tooltipster-sidetip.scada-symbol {
.tooltipster-box {
user-select: none;
background: rgba(0, 0, 0, 0.54);
border: 1px solid rgba(0, 0, 0, 0);
border-radius: 8px;
display: flex;
align-items: center;
.tooltipster-content {
padding: 4px 8px;
font-size: 12px;
line-height: 12px;
font-weight: 500;
color: #ffffff;
}
}
&.tb-active {
.tooltipster-box {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.38);
box-shadow: 0 0 10px 6px rgba(0, 0, 0, .2);
.tooltipster-content {
color: rgba(0, 0, 0, 0.76);
}
}
.tooltipster-arrow {
.tooltipster-arrow-uncropped {
.tooltipster-arrow-background {
border-width: 12px;
}
}
}
&.tooltipster-top {
.tooltipster-arrow {
bottom: -1px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-top-color: rgba(0, 0, 0, 0.38);
}
.tooltipster-arrow-background {
border-top-color: #fff;
left: -2px;
}
}
}
}
&.tooltipster-bottom {
.tooltipster-arrow {
top: -1px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-bottom-color: rgba(0, 0, 0, 0.38);
}
.tooltipster-arrow-background {
border-bottom-color: #fff;
left: -2px;
top: -1px;
}
}
}
}
&.tooltipster-left {
.tooltipster-arrow {
right: -1px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-left-color: rgba(0, 0, 0, 0.38);
}
.tooltipster-arrow-background {
border-left-color: #fff;
top: -2px;
}
}
}
}
&.tooltipster-right {
.tooltipster-arrow {
left: -1px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-right-color: rgba(0, 0, 0, 0.38);
}
.tooltipster-arrow-background {
border-right-color: #fff;
top: -2px;
left: -1px;
}
}
}
}
}
&.tooltipster-left, &.tooltipster-right {
.tooltipster-box {
min-height: 36px;
}
}
&.tooltipster-top {
.tooltipster-arrow {
bottom: -2px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-top-color: rgba(0, 0, 0, 0.54);
}
.tooltipster-arrow-background {
border-top-color: transparent;
}
}
}
}
&.tooltipster-bottom {
.tooltipster-arrow {
top: -2px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-bottom-color: rgba(0, 0, 0, 0.54);
}
.tooltipster-arrow-background {
border-bottom-color: transparent;
}
}
}
}
&.tooltipster-left {
.tooltipster-arrow {
right: -2px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-left-color: rgba(0, 0, 0, 0.54);
}
.tooltipster-arrow-background {
border-left-color: transparent;
}
}
}
}
&.tooltipster-right {
.tooltipster-arrow {
left: -2px;
.tooltipster-arrow-uncropped {
.tooltipster-arrow-border {
border-right-color: rgba(0, 0, 0, 0.54);
}
.tooltipster-arrow-background {
border-right-color: transparent;
}
}
}
}
}
.tb-default, .tb-dark {
/*********************************

Loading…
Cancel
Save