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, "order": 9200,
"name": "SCADA symbols" "name": "SCADA symbols"
}, },
"widgetTypeFqns": [ "widgetTypeFqns": []
"scada_symbol"
]
} }

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..."); log.info("Updating system data...");
dataUpdateService.upgradeRuleNodes(); dataUpdateService.upgradeRuleNodes();
systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.loadSystemScadaSymbols();
installScripts.loadSystemLwm2mResources(); installScripts.loadSystemLwm2mResources();
installScripts.loadSystemImages(); installScripts.loadSystemImages();
if (installScripts.isUpdateImages()) { if (installScripts.isUpdateImages()) {
@ -190,6 +191,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.createAdminSettings(); systemDataLoaderService.createAdminSettings();
systemDataLoaderService.createRandomJwtSettings(); systemDataLoaderService.createRandomJwtSettings();
systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.loadSystemScadaSymbols();
systemDataLoaderService.createOAuth2Templates(); systemDataLoaderService.createOAuth2Templates();
systemDataLoaderService.createQueues(); systemDataLoaderService.createQueues();
systemDataLoaderService.createDefaultNotificationConfigs(); 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(); installScripts.loadSystemWidgets();
} }
@Override
public void loadSystemScadaSymbols() throws Exception {
installScripts.loadSystemScadaSymbols();
}
private User createUser(Authority authority, private User createUser(Authority authority,
TenantId tenantId, TenantId tenantId,
CustomerId customerId, CustomerId customerId,

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

@ -16,6 +16,7 @@
package org.thingsboard.server.service.install; package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@ -26,8 +27,12 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard; 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.ResourceType;
import org.thingsboard.server.common.data.TbResource; 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.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; 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.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; 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.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService; 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.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater; 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 RULE_CHAINS_DIR = "rule_chains";
public static final String WIDGET_TYPES_DIR = "widget_types"; public static final String WIDGET_TYPES_DIR = "widget_types";
public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; 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 OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
public static final String DASHBOARDS_DIR = "dashboards"; public static final String DASHBOARDS_DIR = "dashboards";
public static final String MODELS_LWM2M_DIR = "lwm2m-registry"; public static final String MODELS_LWM2M_DIR = "lwm2m-registry";
public static final String CREDENTIALS_DIR = "credentials"; public static final String CREDENTIALS_DIR = "credentials";
public static final String JSON_EXT = ".json"; public static final String JSON_EXT = ".json";
public static final String SVG_EXT = ".svg";
public static final String XML_EXT = ".xml"; public static final String XML_EXT = ".xml";
@Value("${install.data_dir:}") @Value("${install.data_dir:}")
@ -118,6 +127,9 @@ public class InstallScripts {
@Getter @Setter @Getter @Setter
private boolean updateImages = false; private boolean updateImages = false;
@Autowired
private ImageService imageService;
Path getTenantRuleChainsDir() { Path getTenantRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); 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) { private void deleteSystemWidgetBundle(String bundleAlias) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias); WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) { 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 loadSystemWidgets() throws Exception;
void loadSystemScadaSymbols() throws Exception;
void loadDemoData() throws Exception; void loadDemoData() throws Exception;
void createQueues(); 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); TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force);
String calculateImageEtag(byte[] imageData);
TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag); TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag);
boolean replaceBase64WithImageUrl(HasImage entity, String type); 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); WidgetType findWidgetTypeByTenantIdAndFqn(TenantId tenantId, String fqn);
WidgetTypeDetails findWidgetTypeDetailsByTenantIdAndFqn(TenantId tenantId, String fqn);
void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List<WidgetTypeId> widgetTypeIds); void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List<WidgetTypeId> widgetTypeIds);
void updateWidgetsBundleWidgetFqns(TenantId tenantId, WidgetsBundleId widgetsBundleId, List<String> widgetFqns); 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(); return result.success(success).build();
} }
@Override
public String calculateImageEtag(byte[] imageData) {
return calculateEtag(imageData);
}
@Override @Override
public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) { public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) {
log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, 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)); return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndFqn(tenantId, fqn));
} }
@Override
public WidgetTypeDetails findDetailsByTenantIdAndFqn(UUID tenantId, String fqn) {
return DaoUtil.getData(widgetTypeRepository.findByTenantIdAndFqn(tenantId, fqn));
}
@Override @Override
public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId) { public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndResourceId(tenantId, 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, WidgetTypeEntity findWidgetTypeByTenantIdAndFqn(@Param("tenantId") UUID tenantId,
@Param("fqn") String fqn); @Param("fqn") String fqn);
WidgetTypeDetailsEntity findByTenantIdAndFqn(UUID tenantId, String fqn);
@Query(value = "SELECT * FROM widget_type wt " + @Query(value = "SELECT * FROM widget_type wt " +
"WHERE wt.tenant_id = :tenantId AND cast(wt.descriptor as json) ->> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))", "WHERE wt.tenant_id = :tenantId AND cast(wt.descriptor as json) ->> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))",
nativeQuery = true) 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.Directory;
import com.drew.metadata.Metadata; import com.drew.metadata.Metadata;
import com.drew.metadata.Tag; import com.drew.metadata.Tag;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@ -39,6 +41,7 @@ import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.w3c.dom.Document; import org.w3c.dom.Document;
@ -234,6 +237,17 @@ public class ImageUtils {
return originalImage; 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) { private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension, boolean originalIfSmaller) {
if (originalWidth <= maxDimension && originalHeight <= maxDimension && originalIfSmaller) { if (originalWidth <= maxDimension && originalHeight <= maxDimension && originalIfSmaller) {
return new int[]{originalWidth, originalHeight}; return new int[]{originalWidth, originalHeight};
@ -264,4 +278,33 @@ public class ImageUtils {
private ProcessedImage preview; 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); WidgetType findByTenantIdAndFqn(UUID tenantId, String fqn);
WidgetTypeDetails findDetailsByTenantIdAndFqn(UUID tenantId, String fqn);
/** /**
* Find widget types infos by tenantId and resourceId in descriptor. * 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); 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 @Override
public void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List<WidgetTypeId> widgetTypeIds) { public void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List<WidgetTypeId> widgetTypeIds) {
log.trace("Executing updateWidgetsBundleWidgetTypes, tenantId [{}], widgetsBundleId [{}], widgetTypeIds [{}]", tenantId, widgetsBundleId, 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, ComparisonResultType,
DataKey, DataKey,
DataKeyConfigMode, DataKeyConfigMode,
DatasourceType, JsonSettingsSchema, Widget, DatasourceType,
JsonSettingsSchema,
Widget,
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.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 { TranslateService } from '@ngx-translate/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { import {
@ -51,6 +52,7 @@ import {
aggregatedValueCardKeyPositionTranslations, aggregatedValueCardKeyPositionTranslations,
AggregatedValueCardKeySettings AggregatedValueCardKeySettings
} from '@home/components/widget/lib/cards/aggregated-value-card.models'; } from '@home/components/widget/lib/cards/aggregated-value-card.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
@Component({ @Component({
selector: 'tb-aggregated-data-key-row', selector: 'tb-aggregated-data-key-row',
@ -99,7 +101,7 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn
valuePreviewFn = this._valuePreviewFn.bind(this); valuePreviewFn = this._valuePreviewFn.bind(this);
get callbacks(): DataKeysCallbacks { get callbacks(): WidgetConfigCallbacks {
return this.widgetConfigComponent.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, Component,
EventEmitter, EventEmitter,
forwardRef, forwardRef,
Input, OnChanges, Input,
OnChanges,
OnInit, OnInit,
Output, SimpleChanges, Output,
SimpleChanges,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { import {
@ -45,7 +47,7 @@ import {
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.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 { merge } from 'rxjs';
import { import {
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
@ -60,8 +62,10 @@ import {
TimeSeriesChartSeriesType, TimeSeriesChartSeriesType,
timeSeriesChartSeriesTypeIcons, timeSeriesChartSeriesTypeIcons,
timeSeriesChartSeriesTypes, timeSeriesChartSeriesTypes,
timeSeriesChartSeriesTypeTranslations, TimeSeriesChartYAxisId timeSeriesChartSeriesTypeTranslations,
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models'; } 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; 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; return this.widgetConfigComponent.widgetType;
} }
get callbacks(): DataKeysCallbacks { get callbacks(): WidgetConfigCallbacks {
return this.widgetConfigComponent.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 { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { DataKey, DataKeyConfigMode, Widget, widgetType } from '@shared/models/widget.models'; 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 { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
export interface DataKeyConfigDialogData { export interface DataKeyConfigDialogData {
dataKey: DataKey; dataKey: DataKey;
@ -49,7 +49,7 @@ export interface DataKeyConfigDialogData {
deviceId?: string; deviceId?: string;
entityAliasId?: string; entityAliasId?: string;
showPostProcessing?: boolean; showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks; callbacks?: WidgetConfigCallbacks;
hideDataKeyName?: boolean; hideDataKeyName?: boolean;
hideDataKeyLabel?: boolean; hideDataKeyLabel?: boolean;
hideDataKeyColor?: 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 <tb-widget-settings
[dashboard]="dashboard" [dashboard]="dashboard"
[aliasController]="aliasController" [aliasController]="aliasController"
[dataKeyCallbacks]="callbacks" [callbacks]="callbacks"
[widget]="widget" [widget]="widget"
[widgetConfig]="widgetConfig" [widgetConfig]="widgetConfig"
formControlName="settings"> formControlName="settings">

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

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

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

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

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

@ -75,7 +75,7 @@
[datakeySettingsFunction]="dataKeySettingsFunction" [datakeySettingsFunction]="dataKeySettingsFunction"
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[callbacks]="dataKeysCallbacks" [callbacks]="callbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value" [entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value" [deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="dataKeys"> formControlName="dataKeys">
@ -91,7 +91,7 @@
[datakeySettingsFunction]="dataKeySettingsFunction" [datakeySettingsFunction]="dataKeySettingsFunction"
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[callbacks]="dataKeysCallbacks" [callbacks]="callbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value" [entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value" [deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="latestDataKeys"> 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 { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { DatasourcesComponent } from '@home/components/widget/config/datasources.component'; import { DatasourcesComponent } from '@home/components/widget/config/datasources.component';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
@Component({ @Component({
selector: 'tb-datasource', selector: 'tb-datasource',
@ -82,6 +83,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.widgetConfigComponent.widgetConfigCallbacks; return this.widgetConfigComponent.widgetConfigCallbacks;
} }
public get callbacks(): WidgetConfigCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
public get dataKeysCallbacks(): DataKeysCallbacks { public get dataKeysCallbacks(): DataKeysCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks; 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 { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.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({ @Component({
selector: 'tb-widget-settings', selector: 'tb-widget-settings',
@ -79,7 +80,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
aliasController: IAliasController; aliasController: IAliasController;
@Input() @Input()
dataKeyCallbacks: DataKeysCallbacks; callbacks: WidgetConfigCallbacks;
@Input() @Input()
dashboard: Dashboard; dashboard: Dashboard;
@ -142,9 +143,10 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
this.definedSettingsComponent.aliasController = this.aliasController; this.definedSettingsComponent.aliasController = this.aliasController;
} }
} }
if (propName === 'dataKeyCallbacks') { if (propName === 'callbacks') {
if (this.definedSettingsComponent) { if (this.definedSettingsComponent) {
this.definedSettingsComponent.dataKeyCallbacks = this.dataKeyCallbacks; this.definedSettingsComponent.callbacks = this.callbacks;
this.definedSettingsComponent.dataKeyCallbacks = this.callbacks;
} }
} }
if (propName === 'widgetConfig') { if (propName === 'widgetConfig') {
@ -234,7 +236,8 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory); this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory);
this.definedSettingsComponent = this.definedSettingsComponentRef.instance; this.definedSettingsComponent = this.definedSettingsComponentRef.instance;
this.definedSettingsComponent.aliasController = this.aliasController; 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.dashboard = this.dashboard;
this.definedSettingsComponent.widget = this.widget; this.definedSettingsComponent.widget = this.widget;
this.definedSettingsComponent.widgetConfig = this.widgetConfig; 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. limitations under the License.
--> -->
<div class="tb-scada-symbol-panel" [style.padding]="padding" <div class="tb-scada-symbol-panel" [style.padding]="padding" [style]="backgroundStyle$ | async">
[class.overlay]="overlayEnabled" [style]="backgroundStyle$ | async">
<div class="tb-scada-symbol-overlay" [style]="overlayStyle"></div> <div class="tb-scada-symbol-overlay" [style]="overlayStyle"></div>
<ng-container *ngIf="widgetComponent.dashboardWidget.showWidgetTitlePanel"> <ng-container *ngIf="widgetComponent.dashboardWidget.showWidgetTitlePanel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> <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; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
&.overlay { padding: 20px 24px 24px 24px;
padding: 20px 24px 24px 24px;
}
> div:not(.tb-scada-symbol-overlay) { > div:not(.tb-scada-symbol-overlay) {
z-index: 1; 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>; backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {}; overlayStyle: ComponentStyle = {};
overlayEnabled: boolean;
padding: string; padding: string;
loading$ = this.loadingSubject.asObservable().pipe(share()); 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.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay); this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.overlayEnabled = this.settings.background.overlay.enabled; this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
this.padding = this.overlayEnabled ? undefined : this.settings.padding;
if (this.settings.scadaSymbolContent) { if (this.settings.scadaSymbolContent) {
this.scadaSymbolContent$ = of(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 { export interface ScadaSymbolMetadata {
title: string; title: string;
description?: string;
searchTags?: string[];
stateRenderFunction?: string; stateRenderFunction?: string;
stateRender?: ScadaSymbolStateRenderFunction; stateRender?: ScadaSymbolStateRenderFunction;
tags: ScadaSymbolTag[]; 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 { import {
MobileAppQrCodeWidgetSettingsComponent MobileAppQrCodeWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component'; } 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({ @NgModule({
declarations: [ declarations: [
@ -482,7 +485,8 @@ import {
PieChartWidgetSettingsComponent, PieChartWidgetSettingsComponent,
BarChartWidgetSettingsComponent, BarChartWidgetSettingsComponent,
PolarAreaChartWidgetSettingsComponent, PolarAreaChartWidgetSettingsComponent,
RadarChartWidgetSettingsComponent RadarChartWidgetSettingsComponent,
ScadaSymbolWidgetSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -614,7 +618,8 @@ import {
PieChartWidgetSettingsComponent, PieChartWidgetSettingsComponent,
BarChartWidgetSettingsComponent, BarChartWidgetSettingsComponent,
PolarAreaChartWidgetSettingsComponent, PolarAreaChartWidgetSettingsComponent,
RadarChartWidgetSettingsComponent RadarChartWidgetSettingsComponent,
ScadaSymbolWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {
@ -713,5 +718,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-pie-chart-widget-settings': PieChartWidgetSettingsComponent, 'tb-pie-chart-widget-settings': PieChartWidgetSettingsComponent,
'tb-bar-chart-widget-settings': BarChartWidgetSettingsComponent, 'tb-bar-chart-widget-settings': BarChartWidgetSettingsComponent,
'tb-polar-area-chart-widget-settings': PolarAreaChartWidgetSettingsComponent, '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"> <div *ngIf="displayAdvancedAppearance" style="height: 100%;" [formGroup]="advancedSettings">
<tb-widget-settings <tb-widget-settings
[aliasController]="aliasController" [aliasController]="aliasController"
[dataKeyCallbacks]="widgetConfigCallbacks" [callbacks]="widgetConfigCallbacks"
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[widgetConfig]="modelValue" [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 }}"> <input matInput required formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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"> <div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded> <mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header fxLayout="row wrap"> <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 { ngOnInit(): void {
this.metadataFormGroup = this.fb.group({ this.metadataFormGroup = this.fb.group({
title: [null, [Validators.required]], title: [null, [Validators.required]],
description: [null],
searchTags: [null],
stateRenderFunction: [null], stateRenderFunction: [null],
tags: [null], tags: [null],
behavior: [null], behavior: [null],
@ -170,7 +172,15 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni
writeValue(value: ScadaSymbolMetadata): void { writeValue(value: ScadaSymbolMetadata): void {
this.modelValue = value; this.modelValue = value;
this.metadataFormGroup.patchValue( 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); 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-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; align-items: center;
justify-content: 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 { import {
AfterViewInit, AfterViewInit, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
Input, Input,
@ -28,6 +28,7 @@ import {
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { ScadaSymbolEditObject, ScadaSymbolEditObjectCallbacks } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { ScadaSymbolEditObject, ScadaSymbolEditObjectCallbacks } from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
export interface ScadaSymbolEditorData { export interface ScadaSymbolEditorData {
scadaSymbolContent: string; scadaSymbolContent: string;
@ -44,6 +45,12 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
@ViewChild('scadaSymbolShape', {static: false}) @ViewChild('scadaSymbolShape', {static: false})
scadaSymbolShape: ElementRef<HTMLElement>; scadaSymbolShape: ElementRef<HTMLElement>;
@ViewChild('tooltipsContainer', {static: false})
tooltipsContainer: ElementRef<HTMLElement>;
@ViewChild('tooltipsContainerComponent', {static: true})
tooltipsContainerComponent: TbAnchorComponent;
@Input() @Input()
data: ScadaSymbolEditorData; data: ScadaSymbolEditorData;
@ -55,17 +62,24 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
scadaSymbolEditObject: ScadaSymbolEditObject; scadaSymbolEditObject: ScadaSymbolEditObject;
constructor(private viewContainerRef: ViewContainerRef) { zoomInDisabled = false;
zoomOutDisabled = false;
constructor(private cd: ChangeDetectorRef) {
} }
ngOnInit(): void { ngOnInit(): void {
} }
ngAfterViewInit() { ngAfterViewInit() {
this.editObjectCallbacks.onZoom = () => {
this.updateZoomButtonsState();
};
this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.scadaSymbolShape.nativeElement, 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) { 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 (propName === 'data') {
if (this.scadaSymbolEditObject) { if (this.scadaSymbolEditObject) {
setTimeout(() => { setTimeout(() => {
this.scadaSymbolEditObject.setContent(this.data.scadaSymbolContent); this.updateContent(this.data.scadaSymbolContent);
}); });
} }
} else if (propName === 'readonly') { } else if (propName === 'readonly') {
@ -93,5 +107,26 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
getContent(): string { getContent(): string {
return this.scadaSymbolEditObject?.getContent(); 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; editTagClickAction: (tag: string) => void;
tagsUpdated: (tags: string[]) => void; tagsUpdated: (tags: string[]) => void;
onSymbolEditObjectDirty: (dirty: boolean) => void; onSymbolEditObjectDirty: (dirty: boolean) => void;
onZoom?: () => void;
} }
const minSymbolZoom = 1;
const maxSymbolZoom = 4;
export class ScadaSymbolEditObject { export class ScadaSymbolEditObject {
public svgShape: Svg; public svgShape: Svg;
@ -68,7 +72,10 @@ export class ScadaSymbolEditObject {
public tags: string[] = []; public tags: string[] = [];
private zoomFactor = 0.34;
constructor(private rootElement: HTMLElement, constructor(private rootElement: HTMLElement,
public tooltipsContainer: HTMLElement,
public viewContainerRef: ViewContainerRef, public viewContainerRef: ViewContainerRef,
private callbacks: ScadaSymbolEditObjectCallbacks, private callbacks: ScadaSymbolEditObjectCallbacks,
public readonly: boolean) { public readonly: boolean) {
@ -126,6 +133,40 @@ export class ScadaSymbolEditObject {
this.elements.filter(e => e.isEditing()).forEach(e => e.stopEdit(true)); 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() { private doSetup() {
this.setupZoomPan(0); this.setupZoomPan(0);
(window as any).SVG = svgjs; (window as any).SVG = svgjs;
@ -143,29 +184,37 @@ export class ScadaSymbolEditObject {
detail: { level, focus } detail: { level, focus }
} = e as any; } = e as any;
this.svgShape.zoom(level, focus); this.svgShape.zoom(level, focus);
const box = this.restrictToMargins(this.svgShape.viewbox(), margin); this.postZoom(this.callbacks.onZoom, e, margin);
this.svgShape.viewbox(box);
setTimeout(() => {
this.updateTooltipPositions();
});
e.preventDefault();
}); });
this.svgShape.on('panning', (e) => { this.svgShape.on('panning', (e) => {
const box = (e as any).detail.box; const box = (e as any).detail.box;
this.svgShape.viewbox(this.restrictToMargins(box, margin)); this.svgShape.viewbox(this.restrictToMargins(box, margin));
setTimeout(() => {
this.updateTooltipPositions();
});
e.preventDefault(); e.preventDefault();
}); });
this.svgShape.on('panStart', (_e) => { 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.svgShape.on('panEnd', (_e) => {
this.restoreTooltips();
this.svgShape.node.style.cursor = 'default'; 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 { private restrictToMargins(box: Box, margin: number): Box {
if (box.x < -margin) { if (box.x < -margin) {
box.x = -margin; box.x = -margin;
@ -304,17 +353,29 @@ export class ScadaSymbolEditObject {
} }
private updateZoomOptions() { private updateZoomOptions() {
// this.zoomFactor = 2 / this.scale;
this.svgShape.panZoom({ this.svgShape.panZoom({
zoomMin: 1, zoomMin: minSymbolZoom,
zoomMax: 4, zoomMax: maxSymbolZoom,
zoomFactor: 2 / this.scale zoomFactor: this.zoomFactor
}); });
} }
private updateTooltipPositions() { private updateTooltipPositions() {
const container = this.rootElement.getBoundingClientRect();
for (const e of this.elements) { 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 >= container.left - horizontalGap &&
target.left + target.width <= container.left + container.width + horizontalGap && target.left + target.width <= container.left + container.width + horizontalGap &&
target.top >= container.top - verticalGap && target.top >= container.top - verticalGap &&
target.top + target.height <= container.top + container.height + verticalGap target.top + target.height <= container.top + container.height + verticalGap
); );*/
const elementTooltipMinHeight = 36 + 8; const elementTooltipMinHeight = 36 + 8;
const elementTooltipMinWidth = 100; const elementTooltipMinWidth = 100;
@ -559,21 +620,29 @@ export class ScadaSymbolElement {
return this.editing; return this.editing;
} }
public updateTooltipPosition(container: DOMRect) { public updateTooltipPosition() {
if (this.tooltip && !this.tooltip.status().destroyed) { if (this.tooltip && !this.tooltip.status().destroyed) {
this.tooltip.reposition(); this.tooltip.reposition();
const tooltipElement = this.tooltip.elementTooltip(); const tooltipElement = this.tooltip.elementTooltip();
if (tooltipElement) { if (tooltipElement) {
if (isDomRectContained(tooltipElement.getBoundingClientRect(), container, tooltipElement.style.visibility = null;
elementTooltipMinWidth, elementTooltipMinHeight)) {
tooltipElement.style.visibility = null;
} else {
tooltipElement.style.visibility = 'hidden';
}
} }
} }
} }
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) { public setInnerTooltipOffset(offset: number, center: number) {
this.innerTooltipOffset = offset + (center - this.box.cy) * this.editObject.scale; 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 { private unscaled(size: number): number {
return size / (this.editObject.scale * this.editObject.svgShape.zoom()); return size / (this.editObject.scale * this.editObject.svgShape.zoom());
} }
@ -596,6 +669,7 @@ export class ScadaSymbolElement {
const el = $(this.element.node); const el = $(this.element.node);
el.tooltipster( el.tooltipster(
{ {
parent: this.tooltipContainer,
zIndex: 100, zIndex: 100,
arrow: this.isGroup(), arrow: this.isGroup(),
distance: this.isGroup() ? (this.scaled(groupRectPadding) + groupRectStroke) : 6, distance: this.isGroup() ? (this.scaled(groupRectPadding) + groupRectStroke) : 6,
@ -611,6 +685,7 @@ export class ScadaSymbolElement {
this.innerTagTooltipPosition(instance, helper, position), this.innerTagTooltipPosition(instance, helper, position),
functionReady: (_instance, helper) => { functionReady: (_instance, helper) => {
const tooltipEl = $(helper.tooltip); const tooltipEl = $(helper.tooltip);
// tooltipEl.detach().appendTo($(this.editObject.tooltipsContainer));
tooltipEl.on('mouseenter', () => { tooltipEl.on('mouseenter', () => {
this.highlight(); this.highlight();
}); });
@ -632,6 +707,7 @@ export class ScadaSymbolElement {
const el = $(this.element.node); const el = $(this.element.node);
el.tooltipster( el.tooltipster(
{ {
parent: this.tooltipContainer,
zIndex: 100, zIndex: 100,
arrow: true, arrow: true,
theme: ['scada-symbol', 'tb-active'], 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); const tagSettingsButton = $(this.tagSettingsButton.nativeElement);
tagSettingsButton.tooltipster( tagSettingsButton.tooltipster(
{ {
parent: this.symbolElement.tooltipContainer,
zIndex: 200, zIndex: 200,
arrow: true, arrow: true,
theme: ['scada-symbol', 'tb-active'], theme: ['scada-symbol', 'tb-active'],
@ -264,6 +265,7 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements
const removeTagButton = $(this.removeTagButton.nativeElement); const removeTagButton = $(this.removeTagButton.nativeElement);
removeTagButton.tooltipster( removeTagButton.tooltipster(
{ {
parent: this.symbolElement.tooltipContainer,
zIndex: 200, zIndex: 200,
arrow: true, arrow: true,
theme: ['scada-symbol', 'tb-active'], 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 { NULL_UUID } from '@shared/models/id/has-uuid';
import { HasTenantId } from '@shared/models/entity.models'; import { HasTenantId } from '@shared/models/entity.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.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 { export enum widgetType {
timeseries = 'timeseries', timeseries = 'timeseries',
@ -833,6 +834,7 @@ export interface WidgetSize {
export interface IWidgetSettingsComponent { export interface IWidgetSettingsComponent {
aliasController: IAliasController; aliasController: IAliasController;
callbacks: WidgetConfigCallbacks;
dataKeyCallbacks: DataKeysCallbacks; dataKeyCallbacks: DataKeysCallbacks;
dashboard: Dashboard; dashboard: Dashboard;
widget: Widget; widget: Widget;
@ -851,6 +853,8 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
aliasController: IAliasController; aliasController: IAliasController;
callbacks: WidgetConfigCallbacks;
dataKeyCallbacks: DataKeysCallbacks; dataKeyCallbacks: DataKeysCallbacks;
dashboard: Dashboard; dashboard: Dashboard;

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

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

Loading…
Cancel
Save