From 2500bb2e1c1e54c7277c1f87a39e8905a973a018 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 6 Jun 2024 17:26:41 +0300 Subject: [PATCH] Install SCADA symbols from data folder. UI: SCADA editor -> Add Zoom In/Out buttons, improve tooltips. --- .../system/scada_symbols/level_and_fan.svg | 316 ++++++++++++++++++ .../system/widget_bundles/scada_symbols.json | 4 +- .../system/widget_types/scada_symbol.json | 13 +- .../install/ThingsboardInstallService.java | 2 + .../DefaultSystemDataLoaderService.java | 5 + .../service/install/InstallScripts.java | 96 ++++++ .../install/SystemDataLoaderService.java | 2 + .../server/dao/resource/ImageService.java | 2 + .../server/dao/widget/WidgetTypeService.java | 2 + .../server/dao/resource/BaseImageService.java | 5 + .../dao/sql/widget/JpaWidgetTypeDao.java | 5 + .../dao/sql/widget/WidgetTypeRepository.java | 2 + .../server/dao/util/ImageUtils.java | 43 +++ .../server/dao/widget/WidgetTypeDao.java | 2 + .../dao/widget/WidgetTypeServiceImpl.java | 9 + .../aggregated-data-key-row.component.ts | 8 +- .../basic/common/data-key-row.component.ts | 14 +- .../data-key-config-dialog.component.ts | 4 +- .../config/data-key-config.component.html | 2 +- .../config/data-key-config.component.ts | 12 +- .../widget/config/data-keys.component.ts | 11 +- .../widget/config/datasource.component.html | 4 +- .../widget/config/datasource.component.ts | 5 + .../config/widget-settings.component.ts | 11 +- .../scada/scada-symbol-widget.component.html | 3 +- .../scada/scada-symbol-widget.component.scss | 4 +- .../scada/scada-symbol-widget.component.ts | 4 +- .../widget/lib/scada/scada-symbol.models.ts | 2 + ...cada-symbol-widget-settings.component.html | 46 +++ .../scada-symbol-widget-settings.component.ts | 62 ++++ .../lib/settings/widget-settings.module.ts | 12 +- .../widget/widget-config.component.html | 2 +- .../scada-symbol-metadata.component.html | 19 ++ .../scada-symbol-metadata.component.ts | 12 +- .../scada-symbol-editor.component.html | 37 ++ .../scada-symbol-editor.component.scss | 193 +++++++++++ .../scada-symbol-editor.component.ts | 45 ++- .../scada-symbol-editor.models.ts | 124 +++++-- .../scada-symbol-tooltip.components.ts | 2 + ui-ngx/src/app/shared/models/widget.models.ts | 4 + .../assets/locale/locale.constant-en_US.json | 5 + ui-ngx/src/styles.scss | 150 --------- 42 files changed, 1075 insertions(+), 230 deletions(-) create mode 100644 application/src/main/data/json/system/scada_symbols/level_and_fan.svg create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.ts diff --git a/application/src/main/data/json/system/scada_symbols/level_and_fan.svg b/application/src/main/data/json/system/scada_symbols/level_and_fan.svg new file mode 100644 index 0000000000..0ea27397bb --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/level_and_fan.svg @@ -0,0 +1,316 @@ + +{ + "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" + } + ] +}min + N/A + Level + max + + + On + + + Off + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/scada_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_symbols.json index 47615e6137..8a933b18f8 100644 --- a/application/src/main/data/json/system/widget_bundles/scada_symbols.json +++ b/application/src/main/data/json/system/widget_bundles/scada_symbols.json @@ -7,7 +7,5 @@ "order": 9200, "name": "SCADA symbols" }, - "widgetTypeFqns": [ - "scada_symbol" - ] + "widgetTypeFqns": [] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/scada_symbol.json b/application/src/main/data/json/system/widget_types/scada_symbol.json index e7e56176f9..616ad37cdc 100644 --- a/application/src/main/data/json/system/widget_types/scada_symbol.json +++ b/application/src/main/data/json/system/widget_types/scada_symbol.json @@ -3,7 +3,7 @@ "name": "SCADA symbol", "deprecated": false, "image": null, - "description": "SCADA symbol widget", + "description": "", "descriptor": { "type": "rpc", "sizeX": 3.5, @@ -14,15 +14,10 @@ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '280px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", - "settingsDirective": "", + "settingsDirective": "tb-scada-symbol-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-scada-symbol-basic-config", - "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"iotSvgObject\":{\"behavior\":{},\"properties\":{}},\"background\":{\"type\":\"color\",\"imageUrl\":null,\"color\":\"rgb(255, 255, 255)\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\",\"scadaSymbolUrl\":\"tb-image:ZHJhd2luZy5zdmc=:TGV2ZWwgYW5kIEZhbg==:U0NBREFfU1lNQk9M;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnRiPSJodHRwczovL3RoaW5nc2JvYXJkLmlvL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgo8dGI6bWV0YWRhdGEgeG1sbnM9IiI+PCFbQ0RBVEFbewogICJ0aXRsZSI6ICJMZXZlbCBhbmQgRmFuIiwKICAic3RhdGVSZW5kZXJGdW5jdGlvbiI6ICJ2YXIgc2hvd01pbk1heExldmVsID0gY3R4LnByb3BlcnRpZXMuc2hvd01pbk1heExldmVsO1xudmFyIG1pbkxldmVsRWxlbWVudCA9IGN0eC50YWdzLm1pbkxldmVsWzBdO1xudmFyIG1heExldmVsRWxlbWVudCA9IGN0eC50YWdzLm1heExldmVsWzBdO1xudmFyIG1pbkxldmVsID0gY3R4LnByb3BlcnRpZXMubWluTGV2ZWw7IFxudmFyIG1heExldmVsID0gY3R4LnByb3BlcnRpZXMubWF4TGV2ZWw7XG5cbmlmIChzaG93TWluTWF4TGV2ZWwpIHtcbiAgIFxuICAgdmFyIG1pbk1heExldmVsRm9udCA9IGN0eC5wcm9wZXJ0aWVzLm1pbk1heExldmVsRm9udDtcbiAgIHZhciBtaW5NYXhMZXZlbENvbG9yID0gY3R4LnByb3BlcnRpZXMubWluTWF4TGV2ZWxDb2xvcjtcbiAgIFxuICAgY3R4LmFwaS50ZXh0KG1pbkxldmVsRWxlbWVudCwgbWluTGV2ZWwpO1xuICAgY3R4LmFwaS50ZXh0KG1heExldmVsRWxlbWVudCwgbWF4TGV2ZWwpO1xuICAgXG4gICBjdHguYXBpLmZvbnQobWluTGV2ZWxFbGVtZW50LCBtaW5NYXhMZXZlbEZvbnQsIG1pbk1heExldmVsQ29sb3IpO1xuICAgY3R4LmFwaS5mb250KG1heExldmVsRWxlbWVudCwgbWluTWF4TGV2ZWxGb250LCBtaW5NYXhMZXZlbENvbG9yKTtcbiAgICAgIFxufSBlbHNlIHtcbiAgIG1pbkxldmVsRWxlbWVudC5oaWRlKCk7XG4gICBtYXhMZXZlbEVsZW1lbnQuaGlkZSgpO1xufVxuXG52YXIgZGlzYWJsZWQgPSBjdHgudmFsdWVzLmRpc2FibGVkO1xudmFyIG9uID0gY3R4LnZhbHVlcy5vbjtcbnZhciBsZXZlbCA9IGN0eC52YWx1ZXMubGV2ZWw7XG5cbnZhciBvbkJ1dHRvbiA9IGN0eC50YWdzLm9uQnV0dG9uO1xudmFyIG9mZkJ1dHRvbiA9IGN0eC50YWdzLm9mZkJ1dHRvbjtcbnZhciBsZXZlbFVwQnV0dG9uID0gY3R4LnRhZ3MubGV2ZWxVcEJ1dHRvbjtcbnZhciBsZXZlbERvd25CdXR0b24gPSBjdHgudGFncy5sZXZlbERvd25CdXR0b247XG5cbnZhciBvbkJ1dHRvbkVuYWJsZWQgPSAhZGlzYWJsZWQgJiYgIW9uO1xudmFyIG9mZkJ1dHRvbkVuYWJsZWQgPSAhZGlzYWJsZWQgJiYgb247XG52YXIgbGV2ZWxVcEVuYWJsZWQgPSAhZGlzYWJsZWQgJiYgbGV2ZWwgPCBtYXhMZXZlbDtcbnZhciBsZXZlbERvd25FbmFibGVkID0gIWRpc2FibGVkICYmIGxldmVsID4gbWluTGV2ZWw7XG5cbmlmIChvbkJ1dHRvbkVuYWJsZWQpIHtcbiAgICBjdHguYXBpLmVuYWJsZShvbkJ1dHRvbik7XG4gICAgb25CdXR0b25bMF0uZmluZE9uZSgncmVjdCcpLmF0dHIoe2ZpbGw6ICcjMTJlZDE5J30pO1xufSBlbHNlIHtcbiAgICBjdHguYXBpLmRpc2FibGUob25CdXR0b24pO1xuICAgIG9uQnV0dG9uWzBdLmZpbmRPbmUoJ3JlY3QnKS5hdHRyKHtmaWxsOiAnIzc3Nyd9KTtcbn1cbiAgIFxuaWYgKG9mZkJ1dHRvbkVuYWJsZWQpIHtcbiAgICBjdHguYXBpLmVuYWJsZShvZmZCdXR0b24pO1xuICAgIG9mZkJ1dHRvblswXS5maW5kT25lKCdyZWN0JykuYXR0cih7ZmlsbDogJyNlZDEyMWYnfSk7XG59IGVsc2Uge1xuICAgIGN0eC5hcGkuZGlzYWJsZShvZmZCdXR0b24pO1xuICAgIG9mZkJ1dHRvblswXS5maW5kT25lKCdyZWN0JykuYXR0cih7ZmlsbDogJyM3NzcnfSk7XG59ICAgIFxuXG5cbmlmIChsZXZlbFVwRW5hYmxlZCkge1xuICAgIGN0eC5hcGkuZW5hYmxlKGxldmVsVXBCdXR0b24pO1xuICAgIGxldmVsVXBCdXR0b25bMF0uZmluZE9uZSgncmVjdCcpLmF0dHIoe2ZpbGw6ICcjZmZmJ30pO1xufSBlbHNlIHtcbiAgICBjdHguYXBpLmRpc2FibGUobGV2ZWxVcEJ1dHRvbik7XG4gICAgbGV2ZWxVcEJ1dHRvblswXS5maW5kT25lKCdyZWN0JykuYXR0cih7ZmlsbDogJyM3NzcnfSk7XG59XG4gICBcbmlmIChsZXZlbERvd25FbmFibGVkKSB7XG4gICAgY3R4LmFwaS5lbmFibGUobGV2ZWxEb3duQnV0dG9uKTtcbiAgICBsZXZlbERvd25CdXR0b25bMF0uZmluZE9uZSgncmVjdCcpLmF0dHIoe2ZpbGw6ICcjZmZmJ30pO1xufSBlbHNlIHtcbiAgICBjdHguYXBpLmRpc2FibGUobGV2ZWxEb3duQnV0dG9uKTtcbiAgICBsZXZlbERvd25CdXR0b25bMF0uZmluZE9uZSgncmVjdCcpLmF0dHIoe2ZpbGw6ICcjNzc3J30pO1xufSIsCiAgInRhZ3MiOiBbCiAgICB7CiAgICAgICJ0YWciOiAiZmFuIiwKICAgICAgInN0YXRlUmVuZGVyRnVuY3Rpb24iOiAidmFyIG9uID0gY3R4LnZhbHVlcy5vbjtcbmlmIChvbikge1xuICAgIHZhciBsZXZlbCA9IGN0eC52YWx1ZXMubGV2ZWw7IFxuICAgIHZhciBtaW5MZXZlbCA9IGN0eC5wcm9wZXJ0aWVzLm1pbkxldmVsOyBcbiAgICB2YXIgbWF4TGV2ZWwgPSBjdHgucHJvcGVydGllcy5tYXhMZXZlbDtcblxuICAgIHZhciBzcGVlZCA9IChsZXZlbCAtIG1pbkxldmVsKSAvIChtYXhMZXZlbCAtIG1pbkxldmVsKTtcbiAgICBzcGVlZCA9IE1hdGgubWF4KDAsIE1hdGgubWluKDEsIHNwZWVkKSkqMjsgXG5cbiAgICB2YXIgdCA9IGVsZW1lbnQudGltZWxpbmUoKTtcbiAgICBpZiAoIXQuYWN0aXZlKCkpIHtcbiAgICAgIGN0eC5hcGkuYW5pbWF0ZShlbGVtZW50LCAxMDAwKS5lYXNlKCctJykucm90YXRlKDM2MCkubG9vcCgpO1xuICAgICAgdCA9IGVsZW1lbnQudGltZWxpbmUoKTtcbiAgICB9XG4gICAgdC5zcGVlZChzcGVlZCk7XG59IGVsc2Uge1xuICAgIGVsZW1lbnQudGltZWxpbmUoKS5zdG9wKCk7XG59XG4iLAogICAgICAiYWN0aW9ucyI6IG51bGwKICAgIH0sCiAgICB7CiAgICAgICJ0YWciOiAibGV2ZWwiLAogICAgICAic3RhdGVSZW5kZXJGdW5jdGlvbiI6ICJ2YXIgbGV2ZWwgPSBjdHgudmFsdWVzLmxldmVsOyBcbnZhciBkaXNhYmxlZCA9IGN0eC52YWx1ZXMuZGlzYWJsZWQ7XG52YXIgbWluTGV2ZWwgPSBjdHgucHJvcGVydGllcy5taW5MZXZlbDsgXG52YXIgbWF4TGV2ZWwgPSBjdHgucHJvcGVydGllcy5tYXhMZXZlbDtcblxudmFyIGhlaWdodCA9IChsZXZlbCAtIG1pbkxldmVsKSAvIChtYXhMZXZlbCAtIG1pbkxldmVsKTtcbmhlaWdodCA9IE1hdGgubWF4KDAsIE1hdGgubWluKDEsIGhlaWdodCkpKjgwOyBcblxuY3R4LmFwaS5hbmltYXRlKGVsZW1lbnQsIDIwMCkuYXR0cih7aGVpZ2h0OiBoZWlnaHR9KTtcblxudmFyIGZpbGw7XG5pZiAoZGlzYWJsZWQpIHtcbiAgZmlsbCA9IGN0eC5wcm9wZXJ0aWVzLmRpc2FibGVkTGV2ZWxCYWNrZ3JvdW5kO1xufSBlbHNlIHtcbiAgdmFyIGNvbG9yUHJvY2Vzc29yID0gY3R4LnByb3BlcnRpZXMubGV2ZWxCYWNrZ3JvdW5kO1xuICBjb2xvclByb2Nlc3Nvci51cGRhdGUobGV2ZWwpO1xuICBmaWxsID0gY29sb3JQcm9jZXNzb3IuY29sb3I7XG59XG5cbmVsZW1lbnQuYXR0cih7ZmlsbDogZmlsbH0pOyIsCiAgICAgICJhY3Rpb25zIjogbnVsbAogICAgfSwKICAgIHsKICAgICAgInRhZyI6ICJsZXZlbERvd25CdXR0b24iLAogICAgICAiYWN0aW9ucyI6IHsKICAgICAgICAiY2xpY2siOiB7CiAgICAgICAgICAiYWN0aW9uRnVuY3Rpb24iOiAidmFyIGxldmVsID0gY3R4LnZhbHVlcy5sZXZlbDsgXG52YXIgbWluTGV2ZWwgPSBjdHgucHJvcGVydGllcy5taW5MZXZlbDtcblxudmFyIG5ld0xldmVsID0gTWF0aC5tYXgobWluTGV2ZWwsIGxldmVsIC0gNSk7XG5jdHguYXBpLnNldFZhbHVlKCdsZXZlbCcsIG5ld0xldmVsKTtcbmN0eC5hcGkuY2FsbEFjdGlvbihldmVudCwgJ2xldmVsVXBkYXRlU3RhdGUnLCBuZXdMZXZlbCwge1xuICBlcnJvcjogKCkgPT4ge1xuICAgICBjdHguYXBpLnNldFZhbHVlKCdsZXZlbCcsIGxldmVsKTtcbiAgfVxufSk7IgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgIHsKICAgICAgInRhZyI6ICJsZXZlbFRpdGxlIiwKICAgICAgInN0YXRlUmVuZGVyRnVuY3Rpb24iOiAidmFyIHNob3dMZXZlbFRpdGxlID0gY3R4LnByb3BlcnRpZXMuc2hvd0xldmVsVGl0bGU7XG5cbmlmIChzaG93TGV2ZWxUaXRsZSkge1xuICAgdmFyIGxldmVsVGl0bGUgPSBjdHgucHJvcGVydGllcy5sZXZlbFRpdGxlO1xuICAgdmFyIGxldmVsVGl0bGVGb250ID0gY3R4LnByb3BlcnRpZXMubGV2ZWxUaXRsZUZvbnQ7XG4gICB2YXIgbGV2ZWxUaXRsZUNvbG9yID0gY3R4LnByb3BlcnRpZXMubGV2ZWxUaXRsZUNvbG9yO1xuICAgXG4gICBjdHguYXBpLnRleHQoZWxlbWVudCwgbGV2ZWxUaXRsZSk7XG4gICBjdHguYXBpLmZvbnQoZWxlbWVudCwgbGV2ZWxUaXRsZUZvbnQsIGxldmVsVGl0bGVDb2xvcik7XG4gICBcbn0gZWxzZSB7XG4gICBlbGVtZW50LmhpZGUoKTtcbn0iLAogICAgICAiYWN0aW9ucyI6IG51bGwKICAgIH0sCiAgICB7CiAgICAgICJ0YWciOiAibGV2ZWxVcEJ1dHRvbiIsCiAgICAgICJhY3Rpb25zIjogewogICAgICAgICJjbGljayI6IHsKICAgICAgICAgICJhY3Rpb25GdW5jdGlvbiI6ICJ2YXIgbGV2ZWwgPSBjdHgudmFsdWVzLmxldmVsOyBcbnZhciBtYXhMZXZlbCA9IGN0eC5wcm9wZXJ0aWVzLm1heExldmVsO1xuXG52YXIgbmV3TGV2ZWwgPSBNYXRoLm1pbihtYXhMZXZlbCwgbGV2ZWwgKyA1KTtcbmN0eC5hcGkuc2V0VmFsdWUoJ2xldmVsJywgbmV3TGV2ZWwpO1xuY3R4LmFwaS5jYWxsQWN0aW9uKGV2ZW50LCAnbGV2ZWxVcGRhdGVTdGF0ZScsIG5ld0xldmVsLCB7XG4gIGVycm9yOiAoKSA9PiB7XG4gICAgIGN0eC5hcGkuc2V0VmFsdWUoJ2xldmVsJywgbGV2ZWwpO1xuICB9XG59KTsiCiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgewogICAgICAidGFnIjogImxldmVsVmFsdWUiLAogICAgICAic3RhdGVSZW5kZXJGdW5jdGlvbiI6ICJ2YXIgc2hvd1ZhbHVlID0gY3R4LnByb3BlcnRpZXMuc2hvd1ZhbHVlO1xuXG5pZiAoc2hvd1ZhbHVlKSB7XG4gIHZhciB2YWx1ZUZvbnQgPSBjdHgucHJvcGVydGllcy52YWx1ZUZvbnQ7XG4gIHZhciB2YWx1ZUNvbG9yID0gY3R4LnByb3BlcnRpZXMudmFsdWVDb2xvcjtcbiAgdmFyIGxldmVsID0gY3R4LnZhbHVlcy5sZXZlbDsgXG4gIFxuICB2YXIgbGV2ZWxUZXh0ID0gY3R4LmFwaS5mb3JtYXRWYWx1ZShsZXZlbCwgY3R4LnByb3BlcnRpZXMudmFsdWVEZWNpbWFscywgY3R4LnByb3BlcnRpZXMudmFsdWVVbml0cywgZmFsc2UpO1xuXG4gIGN0eC5hcGkuZm9udChlbGVtZW50LCB2YWx1ZUZvbnQsIHZhbHVlQ29sb3IpO1xuICBjdHguYXBpLnRleHQoZWxlbWVudCwgbGV2ZWxUZXh0KTtcblxufSBlbHNlIHtcbiAgZWxlbWVudC5oaWRlKCk7XG4gIGN0eC50YWdzLmxldmVsVmFsdWVCYWNrZ3JvdW5kWzBdLmhpZGUoKTtcbn0iLAogICAgICAiYWN0aW9ucyI6IHsKICAgICAgICAiY2xpY2siOiB7CiAgICAgICAgICAiYWN0aW9uRnVuY3Rpb24iOiAiY3R4LmFwaS5jYWxsQWN0aW9uKGV2ZW50LCAnbGV2ZWxWYWx1ZUNsaWNrJyk7IgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgIHsKICAgICAgInRhZyI6ICJvZmZCdXR0b24iLAogICAgICAiYWN0aW9ucyI6IHsKICAgICAgICAiY2xpY2siOiB7CiAgICAgICAgICAiYWN0aW9uRnVuY3Rpb24iOiAiY3R4LmFwaS5jYWxsQWN0aW9uKGV2ZW50LCAnb2ZmVXBkYXRlU3RhdGUnLCB1bmRlZmluZWQsIHtcbiAgbmV4dDogKCkgPT4ge1xuICAgICBjdHguYXBpLnNldFZhbHVlKCdvbicsIGZhbHNlKTtcbiAgfVxufSk7XG4iCiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgewogICAgICAidGFnIjogIm9uQnV0dG9uIiwKICAgICAgImFjdGlvbnMiOiB7CiAgICAgICAgImNsaWNrIjogewogICAgICAgICAgImFjdGlvbkZ1bmN0aW9uIjogImN0eC5hcGkuY2FsbEFjdGlvbihldmVudCwgJ29uVXBkYXRlU3RhdGUnLCB1bmRlZmluZWQsIHtcbiAgbmV4dDogKCkgPT4ge1xuICAgICBjdHguYXBpLnNldFZhbHVlKCdvbicsIHRydWUpO1xuICB9XG59KTtcblxuIgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgIHsKICAgICAgInRhZyI6ICJyYW1rYSIsCiAgICAgICJzdGF0ZVJlbmRlckZ1bmN0aW9uIjogInZhciBvbiA9IGN0eC52YWx1ZXMub247XG5pZiAob24pIHtcbiAgICBpZiAoIWVsZW1lbnQudGltZWxpbmUoKS5hY3RpdmUoKSkge1xuICAgICAgICBjdHguYXBpLmFuaW1hdGUoZWxlbWVudCwgMTAwMCkuc2NhbGUoMS4xKS5sb29wKDAsIHRydWUpO1xuICAgIH1cbn0gZWxzZSB7XG4gICAgZWxlbWVudC50aW1lbGluZSgpLnN0b3AoKTtcbn0iLAogICAgICAiYWN0aW9ucyI6IG51bGwKICAgIH0KICBdLAogICJiZWhhdmlvciI6IFsKICAgIHsKICAgICAgImlkIjogImxldmVsIiwKICAgICAgIm5hbWUiOiAiTGV2ZWwiLAogICAgICAiaGludCI6IG51bGwsCiAgICAgICJ0eXBlIjogInZhbHVlIiwKICAgICAgInZhbHVlVHlwZSI6ICJET1VCTEUiLAogICAgICAiZGVmYXVsdFZhbHVlIjogMTAsCiAgICAgICJ0cnVlTGFiZWwiOiBudWxsLAogICAgICAiZmFsc2VMYWJlbCI6IG51bGwsCiAgICAgICJzdGF0ZUxhYmVsIjogbnVsbCwKICAgICAgInZhbHVlVG9EYXRhVHlwZSI6IG51bGwsCiAgICAgICJjb25zdGFudFZhbHVlIjogbnVsbCwKICAgICAgInZhbHVlVG9EYXRhRnVuY3Rpb24iOiBudWxsCiAgICB9LAogICAgewogICAgICAiaWQiOiAiZGlzYWJsZWQiLAogICAgICAibmFtZSI6ICJ7aTE4bjp3aWRnZXRzLnJwYy1zdGF0ZS5kaXNhYmxlZC1zdGF0ZX0iLAogICAgICAiaGludCI6ICJ7aTE4bjp3aWRnZXRzLnJwYy1zdGF0ZS5kaXNhYmxlZC1zdGF0ZS1oaW50fSIsCiAgICAgICJ0eXBlIjogInZhbHVlIiwKICAgICAgInZhbHVlVHlwZSI6ICJCT09MRUFOIiwKICAgICAgImRlZmF1bHRWYWx1ZSI6IGZhbHNlLAogICAgICAidHJ1ZUxhYmVsIjogIiIsCiAgICAgICJmYWxzZUxhYmVsIjogIiIsCiAgICAgICJzdGF0ZUxhYmVsIjogIntpMThuOndpZGdldHMucnBjLXN0YXRlLmRpc2FibGVkfSIsCiAgICAgICJ2YWx1ZVRvRGF0YVR5cGUiOiBudWxsLAogICAgICAiY29uc3RhbnRWYWx1ZSI6IG51bGwsCiAgICAgICJ2YWx1ZVRvRGF0YUZ1bmN0aW9uIjogbnVsbAogICAgfSwKICAgIHsKICAgICAgImlkIjogIm9uIiwKICAgICAgIm5hbWUiOiAiT24vT2ZmIHN0YXRlIiwKICAgICAgImhpbnQiOiBudWxsLAogICAgICAidHlwZSI6ICJ2YWx1ZSIsCiAgICAgICJ2YWx1ZVR5cGUiOiAiQk9PTEVBTiIsCiAgICAgICJkZWZhdWx0VmFsdWUiOiB0cnVlLAogICAgICAidHJ1ZUxhYmVsIjogIntpMThuOndpZGdldHMucnBjLXN0YXRlLm9ufSIsCiAgICAgICJmYWxzZUxhYmVsIjogIntpMThuOndpZGdldHMucnBjLXN0YXRlLm9mZn0iLAogICAgICAic3RhdGVMYWJlbCI6ICJ7aTE4bjp3aWRnZXRzLnJwYy1zdGF0ZS5vbn0iLAogICAgICAidmFsdWVUb0RhdGFUeXBlIjogbnVsbCwKICAgICAgImNvbnN0YW50VmFsdWUiOiBudWxsLAogICAgICAidmFsdWVUb0RhdGFGdW5jdGlvbiI6IG51bGwKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJvblVwZGF0ZVN0YXRlIiwKICAgICAgIm5hbWUiOiAie2kxOG46d2lkZ2V0cy5ycGMtc3RhdGUudHVybi1vbn0iLAogICAgICAiaGludCI6ICJ7aTE4bjp3aWRnZXRzLnJwYy1zdGF0ZS50dXJuLW9uLWhpbnR9IiwKICAgICAgInR5cGUiOiAiYWN0aW9uIiwKICAgICAgInZhbHVlVHlwZSI6ICJCT09MRUFOIiwKICAgICAgInZhbHVlVG9EYXRhVHlwZSI6ICJDT05TVEFOVCIsCiAgICAgICJjb25zdGFudFZhbHVlIjogdHJ1ZQogICAgfSwKICAgIHsKICAgICAgImlkIjogIm9mZlVwZGF0ZVN0YXRlIiwKICAgICAgIm5hbWUiOiAie2kxOG46d2lkZ2V0cy5ycGMtc3RhdGUudHVybi1vZmZ9IiwKICAgICAgImhpbnQiOiAie2kxOG46d2lkZ2V0cy5ycGMtc3RhdGUudHVybi1vZmYtaGludH0iLAogICAgICAidHlwZSI6ICJhY3Rpb24iLAogICAgICAidmFsdWVUeXBlIjogIkJPT0xFQU4iLAogICAgICAidmFsdWVUb0RhdGFUeXBlIjogIkNPTlNUQU5UIiwKICAgICAgImNvbnN0YW50VmFsdWUiOiBmYWxzZQogICAgfSwKICAgIHsKICAgICAgImlkIjogImxldmVsVXBkYXRlU3RhdGUiLAogICAgICAibmFtZSI6ICJVcGRhdGUgbGV2ZWwiLAogICAgICAidHlwZSI6ICJhY3Rpb24iLAogICAgICAidmFsdWVUeXBlIjogIkRPVUJMRSIsCiAgICAgICJ2YWx1ZVRvRGF0YVR5cGUiOiAiVkFMVUUiLAogICAgICAiY29uc3RhbnRWYWx1ZSI6IDAKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJsZXZlbFZhbHVlQ2xpY2siLAogICAgICAibmFtZSI6ICJPbiBsZXZlbCB2YWx1ZSBjbGljayIsCiAgICAgICJ0eXBlIjogIndpZGdldEFjdGlvbiIKICAgIH0KICBdLAogICJwcm9wZXJ0aWVzIjogWwogICAgewogICAgICAiaWQiOiAic2hvd0xldmVsVGl0bGUiLAogICAgICAibmFtZSI6ICJMZXZlbCB0aXRsZSIsCiAgICAgICJ0eXBlIjogInN3aXRjaCIsCiAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgInJvd0NsYXNzIjogImNvbHVtbi14cyIKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJsZXZlbFRpdGxlIiwKICAgICAgIm5hbWUiOiAiTGV2ZWwgdGl0bGUiLAogICAgICAidHlwZSI6ICJ0ZXh0IiwKICAgICAgImRlZmF1bHQiOiAie2kxOG46d2lkZ2V0cy5iYXR0ZXJ5LWxldmVsLnZhbHVlfSIsCiAgICAgICJkaXNhYmxlT25Qcm9wZXJ0eSI6ICJzaG93TGV2ZWxUaXRsZSIsCiAgICAgICJmaWVsZENsYXNzIjogImZsZXgiCiAgICB9LAogICAgewogICAgICAiaWQiOiAibGV2ZWxUaXRsZUZvbnQiLAogICAgICAibmFtZSI6ICJMZXZlbCB0aXRsZSIsCiAgICAgICJ0eXBlIjogImZvbnQiLAogICAgICAiZGVmYXVsdCI6IHsKICAgICAgICAic2l6ZSI6IDYsCiAgICAgICAgInNpemVVbml0IjogInB4IiwKICAgICAgICAiZmFtaWx5IjogIlJvYm90byIsCiAgICAgICAgIndlaWdodCI6ICJub3JtYWwiLAogICAgICAgICJzdHlsZSI6ICJub3JtYWwiCiAgICAgIH0sCiAgICAgICJkaXNhYmxlT25Qcm9wZXJ0eSI6ICJzaG93TGV2ZWxUaXRsZSIKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJsZXZlbFRpdGxlQ29sb3IiLAogICAgICAibmFtZSI6ICJMZXZlbCB0aXRsZSIsCiAgICAgICJ0eXBlIjogImNvbG9yIiwKICAgICAgImRlZmF1bHQiOiAiIzAwMDAwMCIsCiAgICAgICJkaXNhYmxlT25Qcm9wZXJ0eSI6ICJzaG93TGV2ZWxUaXRsZSIKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJzaG93VmFsdWUiLAogICAgICAibmFtZSI6ICJWYWx1ZSIsCiAgICAgICJ0eXBlIjogInN3aXRjaCIsCiAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgInJvd0NsYXNzIjogImNvbHVtbi14cyIKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJ2YWx1ZVVuaXRzIiwKICAgICAgIm5hbWUiOiAiVmFsdWUiLAogICAgICAidHlwZSI6ICJ1bml0cyIsCiAgICAgICJkZWZhdWx0IjogIiIsCiAgICAgICJkaXNhYmxlT25Qcm9wZXJ0eSI6ICJzaG93VmFsdWUiLAogICAgICAiZmllbGRDbGFzcyI6ICJmbGV4IgogICAgfSwKICAgIHsKICAgICAgImlkIjogInZhbHVlRGVjaW1hbHMiLAogICAgICAibmFtZSI6ICJWYWx1ZSIsCiAgICAgICJ0eXBlIjogIm51bWJlciIsCiAgICAgICJkZWZhdWx0IjogMiwKICAgICAgIm1pbiI6IDAsCiAgICAgICJtYXgiOiAxNSwKICAgICAgInN0ZXAiOiAxLAogICAgICAiZmllbGRTdWZmaXgiOiAiZGVjaW1hbHMiLAogICAgICAiZGlzYWJsZU9uUHJvcGVydHkiOiAic2hvd1ZhbHVlIiwKICAgICAgImZpZWxkQ2xhc3MiOiAiZmxleCIKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJ2YWx1ZUZvbnQiLAogICAgICAibmFtZSI6ICJWYWx1ZSIsCiAgICAgICJ0eXBlIjogImZvbnQiLAogICAgICAiZGVmYXVsdCI6IHsKICAgICAgICAic2l6ZSI6IDYsCiAgICAgICAgInNpemVVbml0IjogInB4IiwKICAgICAgICAiZmFtaWx5IjogIlJvYm90byIsCiAgICAgICAgIndlaWdodCI6ICJub3JtYWwiLAogICAgICAgICJzdHlsZSI6ICJub3JtYWwiCiAgICAgIH0sCiAgICAgICJkaXNhYmxlT25Qcm9wZXJ0eSI6ICJzaG93VmFsdWUiCiAgICB9LAogICAgewogICAgICAiaWQiOiAidmFsdWVDb2xvciIsCiAgICAgICJuYW1lIjogIlZhbHVlIiwKICAgICAgInR5cGUiOiAiY29sb3IiLAogICAgICAiZGVmYXVsdCI6ICIjMDAwMDAwIiwKICAgICAgImRpc2FibGVPblByb3BlcnR5IjogInNob3dWYWx1ZSIKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJtaW5MZXZlbCIsCiAgICAgICJuYW1lIjogIkxldmVsIHJhbmdlIiwKICAgICAgInN1YkxhYmVsIjogIm1pbiIsCiAgICAgICJ0eXBlIjogIm51bWJlciIsCiAgICAgICJyZXF1aXJlZCI6IHRydWUsCiAgICAgICJkZWZhdWx0IjogMCwKICAgICAgIm1pbiI6IDAsCiAgICAgICJyb3dDbGFzcyI6ICJjb2x1bW4teHMiLAogICAgICAiZmllbGRDbGFzcyI6ICJmbGV4LXhzIgogICAgfSwKICAgIHsKICAgICAgImlkIjogIm1heExldmVsIiwKICAgICAgIm5hbWUiOiAiTGV2ZWwgcmFuZ2UiLAogICAgICAic3ViTGFiZWwiOiAibWF4IiwKICAgICAgInR5cGUiOiAibnVtYmVyIiwKICAgICAgInJlcXVpcmVkIjogdHJ1ZSwKICAgICAgImRlZmF1bHQiOiAxMDAsCiAgICAgICJtaW4iOiAwLAogICAgICAiZmllbGRDbGFzcyI6ICJmbGV4LXhzIgogICAgfSwKICAgIHsKICAgICAgImlkIjogInNob3dNaW5NYXhMZXZlbCIsCiAgICAgICJuYW1lIjogIk1pbi9NYXggbGFiZWwiLAogICAgICAidHlwZSI6ICJzd2l0Y2giLAogICAgICAiZGVmYXVsdCI6IHRydWUKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJtaW5NYXhMZXZlbEZvbnQiLAogICAgICAibmFtZSI6ICJNaW4vTWF4IGxhYmVsIiwKICAgICAgInR5cGUiOiAiZm9udCIsCiAgICAgICJkZWZhdWx0IjogewogICAgICAgICJzaXplIjogNiwKICAgICAgICAic2l6ZVVuaXQiOiAicHgiLAogICAgICAgICJmYW1pbHkiOiAiUm9ib3RvIiwKICAgICAgICAid2VpZ2h0IjogIm5vcm1hbCIsCiAgICAgICAgInN0eWxlIjogIm5vcm1hbCIKICAgICAgfSwKICAgICAgImRpc2FibGVPblByb3BlcnR5IjogInNob3dNaW5NYXhMZXZlbCIKICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJtaW5NYXhMZXZlbENvbG9yIiwKICAgICAgIm5hbWUiOiAiTWluL01heCBsYWJlbCIsCiAgICAgICJ0eXBlIjogImNvbG9yIiwKICAgICAgImRlZmF1bHQiOiAiIzY2NiIsCiAgICAgICJkaXNhYmxlT25Qcm9wZXJ0eSI6ICJzaG93TWluTWF4TGV2ZWwiCiAgICB9LAogICAgewogICAgICAiaWQiOiAibGV2ZWxCYWNrZ3JvdW5kIiwKICAgICAgIm5hbWUiOiAiTGV2ZWwgYmFja2dyb3VuZCIsCiAgICAgICJzdWJMYWJlbCI6ICJFbmFibGVkIiwKICAgICAgInR5cGUiOiAiY29sb3Jfc2V0dGluZ3MiLAogICAgICAiZGVmYXVsdCI6IHsKICAgICAgICAidHlwZSI6ICJjb25zdGFudCIsCiAgICAgICAgImNvbG9yIjogIiMxYWJiNDgiLAogICAgICAgICJjb2xvckZ1bmN0aW9uIjogInJldHVybiB2YWx1ZSA+IDcwID8gJyNkNTI4MGQnIDogJyMxYWJiNDgnOyIKICAgICAgfSwKICAgICAgImRpdmlkZXIiOiB0cnVlLAogICAgICAicm93Q2xhc3MiOiAiY29sdW1uLXhzIgogICAgfSwKICAgIHsKICAgICAgImlkIjogImRpc2FibGVkTGV2ZWxCYWNrZ3JvdW5kIiwKICAgICAgIm5hbWUiOiAiTGV2ZWwgYmFja2dyb3VuZCIsCiAgICAgICJzdWJMYWJlbCI6ICJEaXNhYmxlZCIsCiAgICAgICJ0eXBlIjogImNvbG9yIiwKICAgICAgImRlZmF1bHQiOiAiI2NjYyIKICAgIH0KICBdCn1dXT48L3RiOm1ldGFkYXRhPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiByeD0iMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjY2NjIiBzdHJva2Utd2lkdGg9IjIiIHRiOnRhZz0icmVjdCIvPjxyZWN0IHg9IjgiIHk9IjE1IiB3aWR0aD0iMjAiIGhlaWdodD0iODEiIHJ4PSIyIiBmaWxsPSIjZWNlY2VjIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMS4wMzU5Ii8+PHJlY3QgdHJhbnNmb3JtPSJzY2FsZSgxIC0xKSIgeD0iOC41IiB5PSItOTUuNSIgd2lkdGg9IjE5IiBoZWlnaHQ9IjQwIiByeD0iMS41IiBmaWxsPSIjMWFiYjQ4IiB0Yjp0YWc9ImxldmVsIi8+PHRleHQgeD0iMzIiIHk9Ijk1IiBmaWxsPSIjNjY2IiBmb250LWZhbWlseT0iUm9ib3RvIiBmb250LXNpemU9IjZweCIgdGI6dGFnPSJtaW5MZXZlbCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjMxLjM1NDMwNyIgeT0iOTYuNjI2MjUxIj5taW48L3RzcGFuPjwvdGV4dD48cmVjdCB4PSIxMC45NDEiIHk9IjQ5LjU0NCIgd2lkdGg9IjE0LjE1NSIgaGVpZ2h0PSIxMiIgcng9IjIuODY2OCIgZmlsbD0iI2ZmZiIgZmlsbC1vcGFjaXR5PSIuNDUxNDgiIHRiOnRhZz0ibGV2ZWxWYWx1ZUJhY2tncm91bmQiLz48ZyBmb250LWZhbWlseT0iUm9ib3RvIiBmb250LXNpemU9IjZweCI+CiAgPHRleHQgeD0iMTgiIHk9IjU2IiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIiBmaWxsPSIjMDAwMDAwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiB0Yjp0YWc9ImxldmVsVmFsdWUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3Bhbj5OL0E8L3RzcGFuPjwvdGV4dD4KICA8dGV4dCB4PSIxNy42MzY4MDUiIHk9IjEwLjM0NzkzMyIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgZmlsbD0iIzAwMDAwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgdGI6dGFnPSJsZXZlbFRpdGxlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4+TGV2ZWw8L3RzcGFuPjwvdGV4dD4KICA8dGV4dCB4PSIzMiIgeT0iMjAiIGZpbGw9IiM2NjY2NjYiIHRiOnRhZz0ibWF4TGV2ZWwiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIyOS44NjU0OTYiIHk9IjE4LjcxNjEyNyI+bWF4PC90c3Bhbj48L3RleHQ+CiA8L2c+PHBhdGggZD0ibTgyLjE0MyAyNy41MzhoLTIwLjQ5OWMtMS4xMDQ1IDAtMS45OTk5IDAuODk1NC0xLjk5OTkgMS45OTk5djIwLjQ5OWMwIDEuMTA0NSAwLjg5NTQgMS45OTk5IDEuOTk5OSAxLjk5OTloMjAuNDk5YzEuMTA0NSAwIDEuOTk5OS0wLjg5NTQgMS45OTk5LTEuOTk5OXYtMjAuNDk5YzAtMS4xMDQ1LTAuODk1NC0xLjk5OTktMS45OTk5LTEuOTk5OXptLTIwLjQ5OSAxLjQ5OTljMC4yNzYxMyAwIDAuNDk5OTggMC4yMjM4NSAwLjQ5OTk4IDAuNDk5OThzLTAuMjIzODUgMC40OTk5OC0wLjQ5OTk4IDAuNDk5OTgtMC40OTk5OC0wLjIyMzg1LTAuNDk5OTgtMC40OTk5OCAwLjIyMzg1LTAuNDk5OTggMC40OTk5OC0wLjQ5OTk4em0wIDIxLjQ5OWMtMC4yNzYxMyAwLTAuNDk5OTgtMC4yMjM4NS0wLjQ5OTk4LTAuNDk5OThzMC4yMjM4NS0wLjQ5OTk4IDAuNDk5OTgtMC40OTk5OCAwLjQ5OTk4IDAuMjIzODUgMC40OTk5OCAwLjQ5OTk4LTAuMjIzODUgMC40OTk5OC0wLjQ5OTk4IDAuNDk5OTh6bTEwLjI0OS0wLjc5OTk3Yy01LjQ5NSAwLTkuOTQ5NS00LjQ1NDUtOS45NDk1LTkuOTQ5NXM0LjQ1NDUtOS45NDk1IDkuOTQ5NS05Ljk0OTUgOS45NDk1IDQuNDU0NSA5Ljk0OTUgOS45NDk1LTQuNDU0NSA5Ljk0OTUtOS45NDk1IDkuOTQ5NXptMTAuMjQ5IDAuNzk5OTdjLTAuMjc2MTMgMC0wLjQ5OTk4LTAuMjIzODUtMC40OTk5OC0wLjQ5OTk4czAuMjIzODUtMC40OTk5OCAwLjQ5OTk4LTAuNDk5OTggMC40OTk5OCAwLjIyMzg1IDAuNDk5OTggMC40OTk5OC0wLjIyMzg1IDAuNDk5OTgtMC40OTk5OCAwLjQ5OTk4em0wLTIwLjQ5OWMtMC4yNzYxMyAwLTAuNDk5OTgtMC4yMjM4NS0wLjQ5OTk4LTAuNDk5OThzMC4yMjM4NS0wLjQ5OTk4IDAuNDk5OTgtMC40OTk5OCAwLjQ5OTk4IDAuMjIzODUgMC40OTk5OCAwLjQ5OTk4LTAuMjIzODUgMC40OTk5OC0wLjQ5OTk4IDAuNDk5OTh6IiBmaWxsPSIjMDgwZDdkIiBzdHJva2Utd2lkdGg9Ii4wNDAwMzEiIHRiOnRhZz0icmFta2EiLz48cGF0aCBkPSJtODAuNjYgMzcuOTgzYy0wLjA0OTU2LTAuMjQwNzgtMC4xNTI4OC0wLjQxMjYtMC4zNDI0Mi0wLjU2OTE2LTEuNTE0OC0xLjI1MTgtMy42MTU0LTEuMTY3Ny02LjMwMjEgMC4yNTIzOS0wLjE3NTMzLTAuMTc1MzMtMC4zNjUxMi0wLjMyMzUzLTAuNTc3OC0wLjQ1MTE0IDEuODUxNy0xLjY2MTIgMy41Mzk2LTIuMzU4NiA1LjA2MzctMi4wOTI0IDAuNDQ5MzQgMC4wNzg1IDAuNzYwNS0wLjQzNjM3IDAuNDgyMDktMC43OTc2NS0wLjYxNzk1LTAuODAyMDEtMS4zMjEyLTEuNDU1LTIuMTY2OC0yLjAxMTktMC4yMDUzNi0wLjEzNTIyLTAuMzk5ODctMC4xODM1OC0wLjY0NDYxLTAuMTYwMzItMS45NTYzIDAuMTg1OTQtMy4zODIyIDEuNzMwOC00LjI3NzggNC42MzQ3LTAuMjQ3OTkgMC0wLjQ4Njk3IDAuMDI5MzgtMC43Mjc1NiAwLjA4OTU1IDAuMTM0Ny0yLjQ4NCAwLjgzNTA0LTQuMTcwNyAyLjEwMS01LjA2MDEgMC4zNzMyNC0wLjI2MjIgMC4yMjkxOC0wLjg0NjI5LTAuMjIzMTMtMC45MDQ5Ny0xLjAwNDEtMC4xMzAxNC0xLjk2MzEtMC4wOTQ1OS0yLjk1NDggMC4xMDk1Ni0wLjI0MDc4IDAuMDQ5NTYtMC40MTI2IDAuMTUyODgtMC41NjkxNiAwLjM0MjQyLTEuMjUxOCAxLjUxNDgtMS4xNjc3IDMuNjE1NCAwLjI1MjM5IDYuMzAyMS0wLjE3NTMzIDAuMTc1MzMtMC4zMjM1MyAwLjM2NTEyLTAuNDUxMTQgMC41Nzc4LTEuNjYxMi0xLjg1MTctMi4zNTg2LTMuNTM5Ni0yLjA5MjQtNS4wNjM3IDAuMDc4NS0wLjQ0OTM0LTAuNDM2MzctMC43NjA1LTAuNzk3NjUtMC40ODIxMy0wLjgwMjAxIDAuNjE3OTUtMS40NTUgMS4zMjEyLTIuMDExOSAyLjE2NjgtMC4xMzUyMiAwLjIwNTM2LTAuMTgzNTggMC4zOTk4Ni0wLjE2MDMyIDAuNjQ0NjEgMC4xODU5NCAxLjk1NjMgMS43MzA4IDMuMzgyMiA0LjYzNDcgNC4yNzc4IDAgMC4yNDc5OSAwLjAyOTM4IDAuNDg2OTcgMC4wODk1NSAwLjcyNzU2LTIuNDg0LTAuMTM0Ny00LjE3MDctMC44MzUwNC01LjA2MDEtMi4xMDEtMC4yNjIyLTAuMzczMjQtMC44NDYyOS0wLjIyOTE4LTAuOTA0OTcgMC4yMjMxMy0wLjEzMDE0IDEuMDA0MS0wLjA5NDU5IDEuOTYzMSAwLjEwOTU2IDIuOTU0OCAwLjA0OTU2IDAuMjQwNzggMC4xNTI4OCAwLjQxMjYgMC4zNDI0MiAwLjU2OTE2IDEuNTE0OCAxLjI1MTggMy42MTU0IDEuMTY3NyA2LjMwMjEtMC4yNTIzOSAwLjE3NTMzIDAuMTc1MzMgMC4zNjUxMiAwLjMyMzUzIDAuNTc3OCAwLjQ1MTE0LTEuODUxNyAxLjY2MTItMy41Mzk2IDIuMzU4Ni01LjA2MzcgMi4wOTI0LTAuNDQ5MzQtMC4wNzg1LTAuNzYwNSAwLjQzNjM3LTAuNDgyMDkgMC43OTc2NSAwLjYxNzk1IDAuODAyMDEgMS4zMjEyIDEuNDU1IDIuMTY2OCAyLjAxMTkgMC4yMDUzNiAwLjEzNTIyIDAuMzk5ODYgMC4xODM1OCAwLjY0NDYxIDAuMTYwMzIgMS45NTYzLTAuMTg1OTQgMy4zODIyLTEuNzMwOCA0LjI3NzgtNC42MzQ3IDAuMjQ3OTkgMCAwLjQ4Njk3LTAuMDI5MzggMC43Mjc1Ni0wLjA4OTU5LTAuMTM0NyAyLjQ4NC0wLjgzNTA0IDQuMTcwNy0yLjEwMSA1LjA2MDEtMC4zNzMyNCAwLjI2MjItMC4yMjkxOCAwLjg0NjI5IDAuMjIzMTMgMC45MDQ5NyAxLjAwNDEgMC4xMzAxNCAxLjk2MzEgMC4wOTQ1OSAyLjk1NDgtMC4xMDk1NiAwLjI0MDc4LTAuMDQ5NTYgMC40MTI2LTAuMTUyODggMC41NjkxNi0wLjM0MjQyIDEuMjUxOC0xLjUxNDggMS4xNjc3LTMuNjE1NC0wLjI1MjM5LTYuMzAyMSAwLjE3NTMzLTAuMTc1MzMgMC4zMjM1My0wLjM2NTEyIDAuNDUxMTQtMC41Nzc4IDEuNjYxMiAxLjg1MTcgMi4zNTg2IDMuNTM5NiAyLjA5MjQgNS4wNjM3LTAuMDc4NSAwLjQ0OTM0IDAuNDM2MzcgMC43NjA1IDAuNzk3NjUgMC40ODIwOSAwLjgwMjAxLTAuNjE3OTUgMS40NTUtMS4zMjEyIDIuMDExOS0yLjE2NjggMC4xMzUyMi0wLjIwNTM2IDAuMTgzNTgtMC4zOTk4NyAwLjE2MDMyLTAuNjQ0NjEtMC4xODU5NC0xLjk1NjMtMS43MzA4LTMuMzgyMi00LjYzNDctNC4yNzc4IDAtMC4yNDc5OS0wLjAyOTM4LTAuNDg2OTctMC4wODk1NS0wLjcyNzU2IDIuNDg0IDAuMTM0NyA0LjE3MDcgMC44MzUwNCA1LjA2MDEgMi4xMDEgMC4yNjIyIDAuMzczMjQgMC44NDYyOSAwLjIyOTE4IDAuOTA0OTctMC4yMjMxMyAwLjEzMDE0LTEuMDA0IDAuMDk0NTktMS45NjMtMC4xMDk1Ni0yLjk1NDd6bS04Ljc2NTcgNC4zMDQzYy0xLjM4MDYgMC0yLjQ5OTktMS4xMTkzLTIuNDk5OS0yLjQ5OTlzMS4xMTkzLTIuNDk5OSAyLjQ5OTktMi40OTk5IDIuNDk5OSAxLjExOTMgMi40OTk5IDIuNDk5OS0xLjExOTMgMi40OTk5LTIuNDk5OSAyLjQ5OTl6IiBmaWxsPSIjN2QwODFlIiBzdHJva2Utd2lkdGg9Ii4wNDAwMzEiIHRiOnRhZz0iZmFuIi8+PGcgdGI6dGFnPSJvbkJ1dHRvbiI+CiAgPHJlY3QgeD0iNTQuNzAyIiB5PSI2MC4zNzIiIHdpZHRoPSIxNC4yNjMiIGhlaWdodD0iNy40MjYxIiByeD0iMS41IiBmaWxsPSIjMTJlZDE5IiBzdHJva2U9IiMwMDAiIHRiOnRhZz0ib25CdXR0b25CYWNrZ3JvdW5kIi8+CiAgPHRleHQgeD0iNjEuODU1NTE4IiB5PSI2NC40OTEyOCIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlJvYm90byIgZm9udC1zaXplPSI0LjQ0NjFweCIgc3Ryb2tlLXdpZHRoPSIuNzQxMDEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiBzdHJva2Utd2lkdGg9Ii43NDEwMSI+T248L3RzcGFuPjwvdGV4dD4KIDwvZz48ZyB0Yjp0YWc9Im9mZkJ1dHRvbiI+CiAgPHJlY3QgeD0iNzQuMzY3IiB5PSI2MC4zMTEiIHdpZHRoPSIxNC4yNjMiIGhlaWdodD0iNy40MjYxIiByeD0iMS41IiBmaWxsPSIjZWQxMjFmIiBzdHJva2U9IiMwMDAiIHRiOnRhZz0ib2ZmQnV0dG9uQmFja2dyb3VuZCIvPgogIDx0ZXh0IHg9IjgxLjM2NTk0NCIgeT0iNjQuNTE4MzQ5IiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iUm9ib3RvIiBmb250LXNpemU9IjQuNDQ2MXB4IiBzdHJva2Utd2lkdGg9Ii43NDEwMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHN0cm9rZS13aWR0aD0iLjc0MTAxIj5PZmY8L3RzcGFuPjwvdGV4dD4KIDwvZz48ZyBzdHJva2U9IiMwMDAiIHN0cm9rZS1taXRlcmxpbWl0PSIxIiB0Yjp0YWc9ImxldmVsVXBCdXR0b24iPgogIDxyZWN0IHg9IjM0Ljk3IiB5PSIzOS4zNzciIHdpZHRoPSI5LjUwMDUiIGhlaWdodD0iMTIuNTg0IiByeD0iLjc1IiBmaWxsPSJub25lIi8+CiAgPHBhdGggdHJhbnNmb3JtPSJtYXRyaXgoLjQ3NTMgLS4yOTM4IC4yOTM4IC40NzUzIDQuODgyMSAzMS4wNCkiIGQ9Im00MS41NDQgNjAuMjQtNy44OTMtNC45NzYyIDguMjU2LTQuMzQ3NC0wLjE4MTUzIDQuNjYxOHoiIGZpbGw9IiMxMmVkMTkiLz4KIDwvZz48ZyBzdHJva2U9IiMwMDAiIHN0cm9rZS1taXRlcmxpbWl0PSIxIiB0Yjp0YWc9ImxldmVsRG93bkJ1dHRvbiI+CiAgPHJlY3QgeD0iMzQuOTciIHk9IjU5LjAzOSIgd2lkdGg9IjkuNTAwNSIgaGVpZ2h0PSIxMi41ODQiIHJ4PSIuNzUiIGZpbGw9Im5vbmUiLz4KICA8cGF0aCB0cmFuc2Zvcm09Im1hdHJpeCgtLjQ3NTE0IC4yOTQwNSAtLjI5NDA1IC0uNDc1MTQgNzQuNTY4IDc5Ljc3MSkiIGQ9Im00MS41NDQgNjAuMjQtNy44OTMtNC45NzYyIDguMjU2LTQuMzQ3NC0wLjE4MTUzIDQuNjYxOHoiIGZpbGw9IiMxMmVkMTkiLz4KIDwvZz4KPC9zdmc+\",\"scadaSymbolObjectSettings\":{\"behavior\":{},\"properties\":{}}},\"title\":\"SCADA symbol\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":null,\"pageSize\":1024,\"titleIcon\":\"mdi:lightbulb-outline\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"targetDevice\":null,\"titleColor\":null,\"borderRadius\":\"0px\",\"margin\":\"0px\"}" + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"background\":{\"type\":\"color\",\"imageUrl\":null,\"color\":\"rgb(255, 255, 255)\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\",\"scadaSymbolUrl\":\"\",\"scadaSymbolObjectSettings\":{\"behavior\":{},\"properties\":{}}},\"title\":\"SCADA symbol\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":null,\"pageSize\":1024,\"titleIcon\":\"mdi:lightbulb-outline\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"targetDevice\":null,\"titleColor\":null,\"borderRadius\":\"0px\",\"margin\":\"0px\"}" }, - "tags": [ - "svg", - "scada", - "SCADA", - "symbol" - ] + "tags": null } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 75ac97881c..839aa61975 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -150,6 +150,7 @@ public class ThingsboardInstallService { log.info("Updating system data..."); dataUpdateService.upgradeRuleNodes(); systemDataLoaderService.loadSystemWidgets(); + systemDataLoaderService.loadSystemScadaSymbols(); installScripts.loadSystemLwm2mResources(); installScripts.loadSystemImages(); if (installScripts.isUpdateImages()) { @@ -190,6 +191,7 @@ public class ThingsboardInstallService { systemDataLoaderService.createAdminSettings(); systemDataLoaderService.createRandomJwtSettings(); systemDataLoaderService.loadSystemWidgets(); + systemDataLoaderService.loadSystemScadaSymbols(); systemDataLoaderService.createOAuth2Templates(); systemDataLoaderService.createQueues(); systemDataLoaderService.createDefaultNotificationConfigs(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 7588726a73..6a150a918b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -526,6 +526,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { installScripts.loadSystemWidgets(); } + @Override + public void loadSystemScadaSymbols() throws Exception { + installScripts.loadSystemScadaSymbols(); + } + private User createUser(Authority authority, TenantId tenantId, CustomerId customerId, diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 9db2a99e7f..ebca8ac4dd 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.install; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; @@ -26,8 +27,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.ImageDescriptor; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -43,8 +48,10 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.util.ImageUtils; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.service.install.update.ImagesUpdater; @@ -84,12 +91,14 @@ public class InstallScripts { public static final String RULE_CHAINS_DIR = "rule_chains"; public static final String WIDGET_TYPES_DIR = "widget_types"; public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; + public static final String SCADA_SYMBOLS_DIR = "scada_symbols"; public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates"; public static final String DASHBOARDS_DIR = "dashboards"; public static final String MODELS_LWM2M_DIR = "lwm2m-registry"; public static final String CREDENTIALS_DIR = "credentials"; public static final String JSON_EXT = ".json"; + public static final String SVG_EXT = ".svg"; public static final String XML_EXT = ".xml"; @Value("${install.data_dir:}") @@ -118,6 +127,9 @@ public class InstallScripts { @Getter @Setter private boolean updateImages = false; + @Autowired + private ImageService imageService; + Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } @@ -290,6 +302,90 @@ public class InstallScripts { } } + public void loadSystemScadaSymbols() throws Exception { + log.info("Loading system SCADA symbols"); + Path scadaSymbolsDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, SCADA_SYMBOLS_DIR); + if (Files.exists(scadaSymbolsDir)) { + WidgetTypeDetails widgetTypeDetailsTemplate = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "scada_symbol"); + List widgetTypeFqns = new ArrayList<>(); + try (DirectoryStream dirStream = Files.newDirectoryStream(scadaSymbolsDir, path -> path.toString().endsWith(SVG_EXT))) { + dirStream.forEach( + path -> { + try { + var fileName = path.getFileName().toString(); + var scadaSymbolData = Files.readAllBytes(path); + var metadata = ImageUtils.processScadaSymbolMetadata(fileName, scadaSymbolData); + TbResourceInfo savedScadaSymbol = saveScadaSymbol(metadata, fileName, scadaSymbolData); + if (widgetTypeDetailsTemplate != null) { + widgetTypeFqns.add(saveScadaSymbolWidget(widgetTypeDetailsTemplate, savedScadaSymbol, metadata)); + } + } catch (Exception e) { + log.error("Unable to load SCADA symbol from file: [{}]", path.toString()); + throw new RuntimeException("Unable to load SCADA symbol from file", e); + } + } + ); + } + var scadaSymbolsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "scada_symbols"); + if (scadaSymbolsBundle != null) { + widgetTypeFqns.add("scada_symbol"); + widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, scadaSymbolsBundle.getId(), widgetTypeFqns); + } + } + } + + private TbResourceInfo saveScadaSymbol(ImageUtils.ScadaSymbolMetadataInfo metadata, String fileName, byte[] scadaSymbolData) { + String etag = imageService.calculateImageEtag(scadaSymbolData); + var existingImage = imageService.findSystemOrTenantImageByEtag(TenantId.SYS_TENANT_ID, etag); + if (existingImage != null && ResourceSubType.SCADA_SYMBOL.equals(existingImage.getResourceSubType())) { + return existingImage; + } else { + TbResource image = new TbResource(); + image.setTenantId(TenantId.SYS_TENANT_ID); + image.setFileName(fileName); + image.setTitle(metadata.getTitle()); + image.setResourceSubType(ResourceSubType.SCADA_SYMBOL); + image.setResourceType(ResourceType.IMAGE); + image.setPublic(true); + ImageDescriptor descriptor = new ImageDescriptor(); + descriptor.setMediaType("image/svg+xml"); + image.setDescriptorValue(descriptor); + image.setData(scadaSymbolData); + return imageService.saveImage(image); + } + } + + private String saveScadaSymbolWidget(WidgetTypeDetails template, TbResourceInfo scadaSymbol, + ImageUtils.ScadaSymbolMetadataInfo metadata) { + String symbolUrl = DataConstants.TB_IMAGE_PREFIX + scadaSymbol.getLink(); + WidgetTypeDetails scadaSymbolWidget = new WidgetTypeDetails(); + JsonNode descriptor = JacksonUtil.clone(template.getDescriptor()); + scadaSymbolWidget.setDescriptor(descriptor); + scadaSymbolWidget.setName(metadata.getTitle()); + scadaSymbolWidget.setImage(symbolUrl); + scadaSymbolWidget.setDescription(metadata.getDescription()); + scadaSymbolWidget.setTags(metadata.getSearchTags()); + ObjectNode defaultConfig = null; + if (descriptor.has("defaultConfig")) { + defaultConfig = JacksonUtil.fromString(descriptor.get("defaultConfig").asText(), ObjectNode.class); + } + if (defaultConfig == null) { + defaultConfig = JacksonUtil.newObjectNode(); + } + defaultConfig.put("title", metadata.getTitle()); + ObjectNode settings; + if (defaultConfig.has("settings")) { + settings = (ObjectNode)defaultConfig.get("settings"); + } else { + settings = JacksonUtil.newObjectNode(); + defaultConfig.set("settings", settings); + } + settings.put("scadaSymbolUrl", symbolUrl); + ((ObjectNode)descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig)); + var savedWidget = widgetTypeService.saveWidgetType(scadaSymbolWidget); + return savedWidget.getFqn(); + } + private void deleteSystemWidgetBundle(String bundleAlias) { WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias); if (widgetsBundle != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index 2f4b5fa885..6711bfa142 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -31,6 +31,8 @@ public interface SystemDataLoaderService { void loadSystemWidgets() throws Exception; + void loadSystemScadaSymbols() throws Exception; + void loadDemoData() throws Exception; void createQueues(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java index e1253ae568..96b97933aa 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java @@ -47,6 +47,8 @@ public interface ImageService { TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force); + String calculateImageEtag(byte[] imageData); + TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag); boolean replaceBase64WithImageUrl(HasImage entity, String type); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java index 98360e4039..8effa18c7c 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java @@ -56,6 +56,8 @@ public interface WidgetTypeService extends EntityDaoService { WidgetType findWidgetTypeByTenantIdAndFqn(TenantId tenantId, String fqn); + WidgetTypeDetails findWidgetTypeDetailsByTenantIdAndFqn(TenantId tenantId, String fqn); + void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetTypeIds); void updateWidgetsBundleWidgetFqns(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetFqns); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index 79427f8d06..0a6c712729 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -287,6 +287,11 @@ public class BaseImageService extends BaseResourceService implements ImageServic return result.success(success).build(); } + @Override + public String calculateImageEtag(byte[] imageData) { + return calculateEtag(imageData); + } + @Override public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) { log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, etag); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index 0adfe1f2be..27c5ae6ca2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java @@ -176,6 +176,11 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId) { return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndResourceId(tenantId, tbResourceId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java index 396e82561c..a87a96ac36 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java @@ -67,6 +67,8 @@ public interface WidgetTypeRepository extends JpaRepository> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))", nativeQuery = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java index db312d7605..675c47002d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java @@ -19,6 +19,8 @@ import com.drew.imaging.ImageMetadataReader; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.Tag; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; @@ -39,6 +41,7 @@ import org.apache.batik.util.XMLResourceDescriptor; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.w3c.dom.Document; @@ -234,6 +237,17 @@ public class ImageUtils { return originalImage; } + public static ScadaSymbolMetadataInfo processScadaSymbolMetadata(String fileName, byte[] data) throws Exception { + SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()); + Document document = factory.createDocument(null, new ByteArrayInputStream(data)); + var metaElements = document.getElementsByTagName("tb:metadata"); + JsonNode metaData = null; + if (metaElements.getLength() > 0) { + metaData = JacksonUtil.toJsonNode(metaElements.item(0).getTextContent()); + } + return new ScadaSymbolMetadataInfo(fileName, metaData); + } + private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension, boolean originalIfSmaller) { if (originalWidth <= maxDimension && originalHeight <= maxDimension && originalIfSmaller) { return new int[]{originalWidth, originalHeight}; @@ -264,4 +278,33 @@ public class ImageUtils { private ProcessedImage preview; } + @Data + public static class ScadaSymbolMetadataInfo { + private String title; + private String description; + private String[] searchTags; + + public ScadaSymbolMetadataInfo(String fileName, JsonNode metaData) { + if (metaData != null && metaData.has("title")) { + title = metaData.get("title").asText(); + } else { + title = fileName; + } + if (metaData != null && metaData.has("description")) { + description = metaData.get("description").asText(); + } else { + description = ""; + } + if (metaData != null && metaData.has("searchTags") && metaData.get("searchTags").isArray()) { + var tagsNode = (ArrayNode) metaData.get("searchTags"); + searchTags = new String[tagsNode.size()]; + for (int i = 0; i < tagsNode.size(); i++) { + searchTags[i] = tagsNode.get(i).asText(); + } + } else { + searchTags = new String[0]; + } + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java index 0a7cabb457..c79dac3095 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java @@ -92,6 +92,8 @@ public interface WidgetTypeDao extends Dao, ExportableEntityD */ WidgetType findByTenantIdAndFqn(UUID tenantId, String fqn); + WidgetTypeDetails findDetailsByTenantIdAndFqn(UUID tenantId, String fqn); + /** * Find widget types infos by tenantId and resourceId in descriptor. * diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index f2506a2d9b..33c9442363 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -189,6 +189,15 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { return widgetTypeDao.findByTenantIdAndFqn(tenantId.getId(), fqn); } + @Override + public WidgetTypeDetails findWidgetTypeDetailsByTenantIdAndFqn(TenantId tenantId, String fqn) { + log.trace("Executing findWidgetTypeDetailsByTenantIdAndFqn, tenantId [{}], fqn [{}]", tenantId, fqn); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateString(fqn, f -> "Incorrect fqn " + f); + return widgetTypeDao.findDetailsByTenantIdAndFqn(tenantId.getId(), fqn); + } + + @Override public void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetTypeIds) { log.trace("Executing updateWidgetsBundleWidgetTypes, tenantId [{}], widgetsBundleId [{}], widgetTypeIds [{}]", tenantId, widgetsBundleId, widgetTypeIds); diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts index 0249d597ae..f5eec3cc1a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts @@ -33,12 +33,13 @@ import { ComparisonResultType, DataKey, DataKeyConfigMode, - DatasourceType, JsonSettingsSchema, Widget, + DatasourceType, + JsonSettingsSchema, + Widget, widgetType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { AggregationType } from '@shared/models/time/time.models'; -import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; import { TranslateService } from '@ngx-translate/core'; import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { @@ -51,6 +52,7 @@ import { aggregatedValueCardKeyPositionTranslations, AggregatedValueCardKeySettings } from '@home/components/widget/lib/cards/aggregated-value-card.models'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; @Component({ selector: 'tb-aggregated-data-key-row', @@ -99,7 +101,7 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn valuePreviewFn = this._valuePreviewFn.bind(this); - get callbacks(): DataKeysCallbacks { + get callbacks(): WidgetConfigCallbacks { return this.widgetConfigComponent.widgetConfigCallbacks; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts index 72eedc21a0..fdd7b292fa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts @@ -19,9 +19,11 @@ import { Component, EventEmitter, forwardRef, - Input, OnChanges, + Input, + OnChanges, OnInit, - Output, SimpleChanges, + Output, + SimpleChanges, ViewEncapsulation } from '@angular/core'; import { @@ -45,7 +47,7 @@ import { widgetType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { merge } from 'rxjs'; import { DataKeyConfigDialogComponent, @@ -60,8 +62,10 @@ import { TimeSeriesChartSeriesType, timeSeriesChartSeriesTypeIcons, timeSeriesChartSeriesTypes, - timeSeriesChartSeriesTypeTranslations, TimeSeriesChartYAxisId + timeSeriesChartSeriesTypeTranslations, + TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; export const dataKeyValid = (key: DataKey): boolean => !!key && !!key.type && !!key.name; @@ -177,7 +181,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan return this.widgetConfigComponent.widgetType; } - get callbacks(): DataKeysCallbacks { + get callbacks(): WidgetConfigCallbacks { return this.widgetConfigComponent.widgetConfigCallbacks; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts index e63dc28f35..3237962134 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts @@ -30,12 +30,12 @@ import { import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { DataKey, DataKeyConfigMode, Widget, widgetType } from '@shared/models/widget.models'; -import { DataKeysCallbacks } from './data-keys.component.models'; import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { TranslateService } from '@ngx-translate/core'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; export interface DataKeyConfigDialogData { dataKey: DataKey; @@ -49,7 +49,7 @@ export interface DataKeyConfigDialogData { deviceId?: string; entityAliasId?: string; showPostProcessing?: boolean; - callbacks?: DataKeysCallbacks; + callbacks?: WidgetConfigCallbacks; hideDataKeyName?: boolean; hideDataKeyLabel?: boolean; hideDataKeyColor?: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html index f472325d8d..f4b1272812 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html @@ -196,7 +196,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts index 9ebe8821cf..4db2aaa8bf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts @@ -22,17 +22,18 @@ import { ComparisonResultType, comparisonResultTypeTranslationMap, DataKey, - dataKeyAggregationTypeHintTranslationMap, DataKeyConfigMode, + dataKeyAggregationTypeHintTranslationMap, + DataKeyConfigMode, Widget, widgetType } from '@shared/models/widget.models'; import { ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - NG_VALIDATORS, - NG_VALUE_ACCESSOR, Validator, Validators } from '@angular/forms'; @@ -40,7 +41,7 @@ import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { EntityService } from '@core/http/entity.service'; -import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { Observable, of } from 'rxjs'; import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; @@ -55,6 +56,7 @@ import { genNextLabel, isDefinedAndNotNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetComponentService } from '@home/components/widget/widget-component.service'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; @Component({ selector: 'tb-data-key-config', @@ -105,7 +107,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con entityAliasId: string; @Input() - callbacks: DataKeysCallbacks; + callbacks: WidgetConfigCallbacks; @Input() dashboard: Dashboard; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts index 2b0ed0657e..e01d7e808a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts @@ -33,13 +33,15 @@ import { import { AbstractControl, ControlValueAccessor, - FormGroupDirective, NG_VALIDATORS, + FormGroupDirective, + NG_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - ValidationErrors, Validator + ValidationErrors, + Validator } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; @@ -52,7 +54,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; import { IAliasController } from '@core/api/widget-api.models'; -import { DataKeysCallbacks, DataKeySettingsFunction } from './data-keys.component.models'; +import { DataKeySettingsFunction } from './data-keys.component.models'; import { alarmFields } from '@shared/models/alarm.models'; import { UtilsService } from '@core/services/utils.service'; import { ErrorStateMatcher } from '@angular/material/core'; @@ -71,6 +73,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { DatasourceComponent } from '@home/components/widget/config/datasource.component'; import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component'; import { TbPopoverService } from '@shared/components/popover.service'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; @Component({ selector: 'tb-data-keys', @@ -166,7 +169,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange widget: Widget; @Input() - callbacks: DataKeysCallbacks; + callbacks: WidgetConfigCallbacks; @Input() entityAliasId: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html index 13a52a295b..a5fb98a1ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -75,7 +75,7 @@ [datakeySettingsFunction]="dataKeySettingsFunction" [dashboard]="dashboard" [widget]="widget" - [callbacks]="dataKeysCallbacks" + [callbacks]="callbacks" [entityAliasId]="datasourceFormGroup.get('entityAliasId').value" [deviceId]="datasourceFormGroup.get('deviceId').value" formControlName="dataKeys"> @@ -91,7 +91,7 @@ [datakeySettingsFunction]="dataKeySettingsFunction" [dashboard]="dashboard" [widget]="widget" - [callbacks]="dataKeysCallbacks" + [callbacks]="callbacks" [entityAliasId]="datasourceFormGroup.get('entityAliasId').value" [deviceId]="datasourceFormGroup.get('deviceId').value" formControlName="latestDataKeys"> diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts index c134b5580c..6038982d48 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts @@ -42,6 +42,7 @@ import { FilterSelectCallbacks } from '@home/components/filter/filter-select.com import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { EntityType } from '@shared/models/entity-type.models'; import { DatasourcesComponent } from '@home/components/widget/config/datasources.component'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; @Component({ selector: 'tb-datasource', @@ -82,6 +83,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida return this.widgetConfigComponent.widgetConfigCallbacks; } + public get callbacks(): WidgetConfigCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + public get dataKeysCallbacks(): DataKeysCallbacks { return this.widgetConfigComponent.widgetConfigCallbacks; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts index 8636db1f77..e1ff910ab2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts @@ -50,6 +50,7 @@ import { WidgetService } from '@core/http/widget.service'; import { IAliasController } from '@core/api/widget-api.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; @Component({ selector: 'tb-widget-settings', @@ -79,7 +80,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On aliasController: IAliasController; @Input() - dataKeyCallbacks: DataKeysCallbacks; + callbacks: WidgetConfigCallbacks; @Input() dashboard: Dashboard; @@ -142,9 +143,10 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On this.definedSettingsComponent.aliasController = this.aliasController; } } - if (propName === 'dataKeyCallbacks') { + if (propName === 'callbacks') { if (this.definedSettingsComponent) { - this.definedSettingsComponent.dataKeyCallbacks = this.dataKeyCallbacks; + this.definedSettingsComponent.callbacks = this.callbacks; + this.definedSettingsComponent.dataKeyCallbacks = this.callbacks; } } if (propName === 'widgetConfig') { @@ -234,7 +236,8 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory); this.definedSettingsComponent = this.definedSettingsComponentRef.instance; this.definedSettingsComponent.aliasController = this.aliasController; - this.definedSettingsComponent.dataKeyCallbacks = this.dataKeyCallbacks; + this.definedSettingsComponent.callbacks = this.callbacks; + this.definedSettingsComponent.dataKeyCallbacks = this.callbacks; this.definedSettingsComponent.dashboard = this.dashboard; this.definedSettingsComponent.widget = this.widget; this.definedSettingsComponent.widgetConfig = this.widgetConfig; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html index bece57e381..55921d0e0b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html @@ -15,8 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.scss index 13543851ad..5d512f6ef7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.scss @@ -21,9 +21,7 @@ display: flex; flex-direction: column; gap: 8px; - &.overlay { - padding: 20px 24px 24px 24px; - } + padding: 20px 24px 24px 24px; > div:not(.tb-scada-symbol-overlay) { z-index: 1; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts index 7f275cd09a..92c5f08f3d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts +++ b/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; overlayStyle: ComponentStyle = {}; - overlayEnabled: boolean; padding: string; loading$ = this.loadingSubject.asObservable().pipe(share()); @@ -84,8 +83,7 @@ export class ScadaSymbolWidgetComponent implements OnInit, AfterViewInit, OnDest this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); - this.overlayEnabled = this.settings.background.overlay.enabled; - this.padding = this.overlayEnabled ? undefined : this.settings.padding; + this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; if (this.settings.scadaSymbolContent) { this.scadaSymbolContent$ = of(this.settings.scadaSymbolContent); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 82dd2de1cc..3742f24082 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -175,6 +175,8 @@ export type ScadaSymbolProperty = ScadaSymbolPropertyBase & ScadaSymbolNumberPro export interface ScadaSymbolMetadata { title: string; + description?: string; + searchTags?: string[]; stateRenderFunction?: string; stateRender?: ScadaSymbolStateRenderFunction; tags: ScadaSymbolTag[]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.html new file mode 100644 index 0000000000..7756989ab9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.html @@ -0,0 +1,46 @@ + + +
+
scada.symbol
+ +
+ + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component.ts new file mode 100644 index 0000000000..609c88ee0f --- /dev/null +++ b/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, + 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, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 461c92bfea..e259acb951 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -356,6 +356,9 @@ import { import { MobileAppQrCodeWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component'; +import { + ScadaSymbolWidgetSettingsComponent +} from '@home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component'; @NgModule({ declarations: [ @@ -482,7 +485,8 @@ import { PieChartWidgetSettingsComponent, BarChartWidgetSettingsComponent, PolarAreaChartWidgetSettingsComponent, - RadarChartWidgetSettingsComponent + RadarChartWidgetSettingsComponent, + ScadaSymbolWidgetSettingsComponent ], imports: [ CommonModule, @@ -614,7 +618,8 @@ import { PieChartWidgetSettingsComponent, BarChartWidgetSettingsComponent, PolarAreaChartWidgetSettingsComponent, - RadarChartWidgetSettingsComponent + RadarChartWidgetSettingsComponent, + ScadaSymbolWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -713,5 +718,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type
+
+
scada.description
+ + + +
+
+
scada.search-tags
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index 788ab4b17a..0d6e6771ce 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -128,6 +128,8 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni ngOnInit(): void { this.metadataFormGroup = this.fb.group({ title: [null, [Validators.required]], + description: [null], + searchTags: [null], stateRenderFunction: [null], tags: [null], behavior: [null], @@ -170,7 +172,15 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni writeValue(value: ScadaSymbolMetadata): void { this.modelValue = value; this.metadataFormGroup.patchValue( - value, {emitEvent: false} + { + title: value?.title, + description: value?.description, + searchTags: value?.searchTags, + stateRenderFunction: value?.stateRenderFunction, + tags: value?.tags, + behavior: value?.behavior, + properties: value?.properties + }, {emitEvent: false} ); this.updateFunctionCompleters(value); } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html index 3f73cfbcbd..e2e272cda3 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html @@ -16,3 +16,40 @@ -->
+
+ +
+
+
+ + +
+
+
+ +
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss index cbdd38f7b0..05909caf90 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss @@ -20,3 +20,196 @@ align-items: center; justify-content: center; } +.tb-scada-symbol-editor-tooltips { + position: absolute; + inset: 0; + pointer-events: none; +} +.tb-scada-symbol-editor-buttons { + pointer-events: none; + position: absolute; + left: 20px; + top: 20px; + right: 20px; + display: flex; + justify-content: space-between; + align-items: flex-start; + + .tb-scada-symbol-editor-zoom-buttons { + display: flex; + flex-direction: column; + z-index: 101; + background: #fff; + border-radius: 24px; + &.tb-primary-fill { + &:before { + opacity: 0.1; + } + } + } + + .tb-scada-symbol-editor-upload-buttons { + display: flex; + flex-direction: row; + gap: 8px; + z-index: 101; + .tb-primary-fill { + background: #fff; + border-radius: 50%; + &:before { + opacity: 0.1; + } + } + } +} + +.tooltipster-sidetip.scada-symbol { + position: fixed; + .tooltipster-box { + user-select: none; + background: rgba(0, 0, 0, 0.54); + border: 1px solid rgba(0, 0, 0, 0); + border-radius: 8px; + display: flex; + align-items: center; + .tooltipster-content { + padding: 4px 8px; + font-size: 12px; + line-height: 12px; + font-weight: 500; + color: #ffffff; + } + } + &.tb-active { + .tooltipster-box { + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.38); + box-shadow: 0 0 10px 6px rgba(0, 0, 0, .2); + .tooltipster-content { + color: rgba(0, 0, 0, 0.76); + } + } + .tooltipster-arrow { + .tooltipster-arrow-uncropped { + .tooltipster-arrow-background { + border-width: 12px; + } + } + } + &.tooltipster-top { + .tooltipster-arrow { + bottom: -1px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-top-color: rgba(0, 0, 0, 0.38); + } + .tooltipster-arrow-background { + border-top-color: #fff; + left: -2px; + } + } + } + } + &.tooltipster-bottom { + .tooltipster-arrow { + top: -1px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-bottom-color: rgba(0, 0, 0, 0.38); + } + .tooltipster-arrow-background { + border-bottom-color: #fff; + left: -2px; + top: -1px; + } + } + } + } + &.tooltipster-left { + .tooltipster-arrow { + right: -1px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-left-color: rgba(0, 0, 0, 0.38); + } + .tooltipster-arrow-background { + border-left-color: #fff; + top: -2px; + } + } + } + } + &.tooltipster-right { + .tooltipster-arrow { + left: -1px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-right-color: rgba(0, 0, 0, 0.38); + } + .tooltipster-arrow-background { + border-right-color: #fff; + top: -2px; + left: -1px; + } + } + } + } + } + &.tooltipster-left, &.tooltipster-right { + .tooltipster-box { + min-height: 36px; + } + } + &.tooltipster-top { + .tooltipster-arrow { + bottom: -2px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-top-color: rgba(0, 0, 0, 0.54); + } + .tooltipster-arrow-background { + border-top-color: transparent; + } + } + } + } + &.tooltipster-bottom { + .tooltipster-arrow { + top: -2px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-bottom-color: rgba(0, 0, 0, 0.54); + } + .tooltipster-arrow-background { + border-bottom-color: transparent; + } + } + } + } + &.tooltipster-left { + .tooltipster-arrow { + right: -2px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-left-color: rgba(0, 0, 0, 0.54); + } + .tooltipster-arrow-background { + border-left-color: transparent; + } + } + } + } + &.tooltipster-right { + .tooltipster-arrow { + left: -2px; + .tooltipster-arrow-uncropped { + .tooltipster-arrow-border { + border-right-color: rgba(0, 0, 0, 0.54); + } + .tooltipster-arrow-background { + border-right-color: transparent; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts index 41e8ebb735..84cc868749 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts @@ -15,7 +15,7 @@ /// import { - AfterViewInit, + AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, @@ -28,6 +28,7 @@ import { ViewEncapsulation } from '@angular/core'; import { ScadaSymbolEditObject, ScadaSymbolEditObjectCallbacks } from '@home/pages/scada-symbol/scada-symbol-editor.models'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; export interface ScadaSymbolEditorData { scadaSymbolContent: string; @@ -44,6 +45,12 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI @ViewChild('scadaSymbolShape', {static: false}) scadaSymbolShape: ElementRef; + @ViewChild('tooltipsContainer', {static: false}) + tooltipsContainer: ElementRef; + + @ViewChild('tooltipsContainerComponent', {static: true}) + tooltipsContainerComponent: TbAnchorComponent; + @Input() data: ScadaSymbolEditorData; @@ -55,17 +62,24 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI scadaSymbolEditObject: ScadaSymbolEditObject; - constructor(private viewContainerRef: ViewContainerRef) { + zoomInDisabled = false; + zoomOutDisabled = false; + + constructor(private cd: ChangeDetectorRef) { } ngOnInit(): void { } ngAfterViewInit() { + this.editObjectCallbacks.onZoom = () => { + this.updateZoomButtonsState(); + }; this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.scadaSymbolShape.nativeElement, - this.viewContainerRef, this.editObjectCallbacks, this.readonly); + this.tooltipsContainer.nativeElement, + this.tooltipsContainerComponent.viewContainerRef, this.editObjectCallbacks, this.readonly); if (this.data) { - this.scadaSymbolEditObject.setContent(this.data.scadaSymbolContent); + this.updateContent(this.data.scadaSymbolContent); } } @@ -76,7 +90,7 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI if (propName === 'data') { if (this.scadaSymbolEditObject) { setTimeout(() => { - this.scadaSymbolEditObject.setContent(this.data.scadaSymbolContent); + this.updateContent(this.data.scadaSymbolContent); }); } } else if (propName === 'readonly') { @@ -93,5 +107,26 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI getContent(): string { return this.scadaSymbolEditObject?.getContent(); } + + zoomIn() { + this.scadaSymbolEditObject.zoomIn(); + } + + zoomOut() { + this.scadaSymbolEditObject.zoomOut(); + } + + private updateContent(content: string) { + this.scadaSymbolEditObject.setContent(content); + setTimeout(() => { + this.updateZoomButtonsState(); + }); + } + + private updateZoomButtonsState() { + this.zoomInDisabled = this.scadaSymbolEditObject.zoomInDisabled(); + this.zoomOutDisabled = this.scadaSymbolEditObject.zoomOutDisabled(); + this.cd.markForCheck(); + } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts index e891baec41..c60cade6ad 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts @@ -53,8 +53,12 @@ export interface ScadaSymbolEditObjectCallbacks { editTagClickAction: (tag: string) => void; tagsUpdated: (tags: string[]) => void; onSymbolEditObjectDirty: (dirty: boolean) => void; + onZoom?: () => void; } +const minSymbolZoom = 1; +const maxSymbolZoom = 4; + export class ScadaSymbolEditObject { public svgShape: Svg; @@ -68,7 +72,10 @@ export class ScadaSymbolEditObject { public tags: string[] = []; + private zoomFactor = 0.34; + constructor(private rootElement: HTMLElement, + public tooltipsContainer: HTMLElement, public viewContainerRef: ViewContainerRef, private callbacks: ScadaSymbolEditObjectCallbacks, public readonly: boolean) { @@ -126,6 +133,40 @@ export class ScadaSymbolEditObject { this.elements.filter(e => e.isEditing()).forEach(e => e.stopEdit(true)); } + public zoomIn() { + const level = Math.min(Math.pow(1 + this.zoomFactor, 1.2) * this.svgShape.zoom(), maxSymbolZoom); + this.zoomAnimate(level); + } + + public zoomOut() { + const level = Math.max(Math.pow(1 + this.zoomFactor, -1.2) * this.svgShape.zoom(), minSymbolZoom); + this.zoomAnimate(level); + } + + private zoomAnimate(level: number, animationMs = 200) { + if (level !== this.svgShape.zoom()) { + this.hideTooltips(); + const runner = this.svgShape.animate(animationMs).ease('<>'); + runner + .zoom(level) + .during(() => { + const box = this.restrictToMargins(this.svgShape.viewbox(), 0); + this.svgShape.viewbox(box); + }) + .after(() => { + this.postZoom(this.callbacks.onZoom); + }); + } + } + + public zoomInDisabled() { + return Number(this.svgShape.zoom().toFixed(5)) >= maxSymbolZoom; + } + + public zoomOutDisabled() { + return Number(this.svgShape.zoom().toFixed(5)) <= minSymbolZoom; + } + private doSetup() { this.setupZoomPan(0); (window as any).SVG = svgjs; @@ -143,29 +184,37 @@ export class ScadaSymbolEditObject { detail: { level, focus } } = e as any; this.svgShape.zoom(level, focus); - const box = this.restrictToMargins(this.svgShape.viewbox(), margin); - this.svgShape.viewbox(box); - setTimeout(() => { - this.updateTooltipPositions(); - }); - e.preventDefault(); + this.postZoom(this.callbacks.onZoom, e, margin); }); this.svgShape.on('panning', (e) => { const box = (e as any).detail.box; this.svgShape.viewbox(this.restrictToMargins(box, margin)); - setTimeout(() => { - this.updateTooltipPositions(); - }); e.preventDefault(); }); this.svgShape.on('panStart', (_e) => { - this.svgShape.node.style.cursor = 'grab'; + if (this.svgShape.zoom() > minSymbolZoom) { + this.hideTooltips(); + this.svgShape.node.style.cursor = 'grab'; + } }); this.svgShape.on('panEnd', (_e) => { + this.restoreTooltips(); this.svgShape.node.style.cursor = 'default'; }); } + private postZoom(callback?: () => void, e?: any, margin = 0) { + const box = this.restrictToMargins(this.svgShape.viewbox(), margin); + this.svgShape.viewbox(box); + setTimeout(() => { + this.updateTooltipPositions(); + }); + e?.preventDefault(); + if (callback) { + callback(); + } + } + private restrictToMargins(box: Box, margin: number): Box { if (box.x < -margin) { box.x = -margin; @@ -304,17 +353,29 @@ export class ScadaSymbolEditObject { } private updateZoomOptions() { + // this.zoomFactor = 2 / this.scale; this.svgShape.panZoom({ - zoomMin: 1, - zoomMax: 4, - zoomFactor: 2 / this.scale + zoomMin: minSymbolZoom, + zoomMax: maxSymbolZoom, + zoomFactor: this.zoomFactor }); } private updateTooltipPositions() { - const container = this.rootElement.getBoundingClientRect(); for (const e of this.elements) { - e.updateTooltipPosition(container); + e.updateTooltipPosition(); + } + } + + private hideTooltips() { + for (const e of this.elements) { + e.hideTooltip(); + } + } + + private restoreTooltips() { + for (const e of this.elements) { + e.restoreTooltip(); } } @@ -362,12 +423,12 @@ const hasBBox = (e: Element): boolean => { } }; -const isDomRectContained = (target: DOMRect, container: DOMRect, horizontalGap = 0, verticalGap = 0): boolean => ( +/*const isDomRectContained = (target: DOMRect, container: DOMRect, horizontalGap = 0, verticalGap = 0): boolean => ( target.left >= container.left - horizontalGap && target.left + target.width <= container.left + container.width + horizontalGap && target.top >= container.top - verticalGap && target.top + target.height <= container.top + container.height + verticalGap -); +);*/ const elementTooltipMinHeight = 36 + 8; const elementTooltipMinWidth = 100; @@ -559,21 +620,29 @@ export class ScadaSymbolElement { return this.editing; } - public updateTooltipPosition(container: DOMRect) { + public updateTooltipPosition() { if (this.tooltip && !this.tooltip.status().destroyed) { this.tooltip.reposition(); const tooltipElement = this.tooltip.elementTooltip(); if (tooltipElement) { - if (isDomRectContained(tooltipElement.getBoundingClientRect(), container, - elementTooltipMinWidth, elementTooltipMinHeight)) { - tooltipElement.style.visibility = null; - } else { - tooltipElement.style.visibility = 'hidden'; - } + tooltipElement.style.visibility = null; } } } + public hideTooltip() { + if (this.tooltip && !this.tooltip.status().destroyed) { + const tooltipElement = this.tooltip.elementTooltip(); + if (tooltipElement) { + tooltipElement.style.visibility = 'hidden'; + } + } + } + + public restoreTooltip() { + this.updateTooltipPosition(); + } + public setInnerTooltipOffset(offset: number, center: number) { this.innerTooltipOffset = offset + (center - this.box.cy) * this.editObject.scale; } @@ -584,6 +653,10 @@ export class ScadaSymbolElement { } } + public get tooltipContainer(): JQuery { + return $(this.editObject.tooltipsContainer); + } + private unscaled(size: number): number { return size / (this.editObject.scale * this.editObject.svgShape.zoom()); } @@ -596,6 +669,7 @@ export class ScadaSymbolElement { const el = $(this.element.node); el.tooltipster( { + parent: this.tooltipContainer, zIndex: 100, arrow: this.isGroup(), distance: this.isGroup() ? (this.scaled(groupRectPadding) + groupRectStroke) : 6, @@ -611,6 +685,7 @@ export class ScadaSymbolElement { this.innerTagTooltipPosition(instance, helper, position), functionReady: (_instance, helper) => { const tooltipEl = $(helper.tooltip); + // tooltipEl.detach().appendTo($(this.editObject.tooltipsContainer)); tooltipEl.on('mouseenter', () => { this.highlight(); }); @@ -632,6 +707,7 @@ export class ScadaSymbolElement { const el = $(this.element.node); el.tooltipster( { + parent: this.tooltipContainer, zIndex: 100, arrow: true, theme: ['scada-symbol', 'tb-active'], diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts index d6afec4932..d1b9278816 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -244,6 +244,7 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements const tagSettingsButton = $(this.tagSettingsButton.nativeElement); tagSettingsButton.tooltipster( { + parent: this.symbolElement.tooltipContainer, zIndex: 200, arrow: true, theme: ['scada-symbol', 'tb-active'], @@ -264,6 +265,7 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements const removeTagButton = $(this.removeTagButton.nativeElement); removeTagButton.tooltipster( { + parent: this.symbolElement.tooltipContainer, zIndex: 200, arrow: true, theme: ['scada-symbol', 'tb-active'], diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 20f2062c77..88d537220e 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -43,6 +43,7 @@ import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-set import { NULL_UUID } from '@shared/models/id/has-uuid'; import { HasTenantId } from '@shared/models/entity.models'; import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; export enum widgetType { timeseries = 'timeseries', @@ -833,6 +834,7 @@ export interface WidgetSize { export interface IWidgetSettingsComponent { aliasController: IAliasController; + callbacks: WidgetConfigCallbacks; dataKeyCallbacks: DataKeysCallbacks; dashboard: Dashboard; widget: Widget; @@ -851,6 +853,8 @@ export abstract class WidgetSettingsComponent extends PageComponent implements aliasController: IAliasController; + callbacks: WidgetConfigCallbacks; + dataKeyCallbacks: DataKeysCallbacks; dashboard: Dashboard; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 6734e1b2d6..c9cf8ef663 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3432,6 +3432,7 @@ "export-symbol": "Export SCADA symbol to JSON", "import-symbol": "Import SCADA symbol from JSON", "upload-symbol": "Upload SCADA symbol", + "update-symbol": "Update SCADA symbol", "edit-symbol": "Edit SCADA symbol", "symbol-details": "SCADA symbol details", "no-symbols": "No symbols found", @@ -3446,6 +3447,8 @@ "tags": "Tags", "properties": "Properties", "title": "Title", + "description": "Description", + "search-tags": "Search tags", "state-render-function": "State render function", "preview": "Preview", "preview-widget-action-text": "Widget action '{{type}}' successfully invoked!", @@ -3453,6 +3456,8 @@ "no-symbol-selected": "No SCADA symbol selected", "clear-symbol": "Clear SCADA symbol", "browse-symbol-from-gallery": "Browse SCADA symbol from gallery", + "zoom-in": "Zoom In", + "zoom-out": "Zoom Out", "tag": { "tag": "Tag", "on-click-action": "On click action", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index d9346ce416..c1465ceb3c 100644 --- a/ui-ngx/src/styles.scss +++ b/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 { /*********************************