diff --git a/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg new file mode 100644 index 0000000000..93c496f2e3 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg @@ -0,0 +1,970 @@ + +{ + "title": "Bottom flow meter", + "description": "Bottom flow meter component used to display flow related value and render various states. Includes pipe fluid and leak visualizations.", + "searchTags": [ + "meter", + "flow meter" + ], + "widgetSizeX": 2, + "widgetSizeY": 2, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.backgroundColor;\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "border", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.defaultBorderColor;\nif (ctx.values.critical) {\n colorProcessor = ctx.properties.criticalBorderColor;\n} else if (ctx.values.warning) {\n colorProcessor = ctx.properties.warningBorderColor;\n} else if (value) {\n colorProcessor = ctx.properties.activeBorderColor;\n}\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "broken", + "stateRenderFunction": "var broken = ctx.values.broken;\nif (broken) {\n element.show();\n} else {\n element.hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "value", + "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "valueUnits", + "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + } + ], + "behavior": [ + { + "id": "value", + "name": "{i18n:scada.symbol.value}", + "hint": "{i18n:scada.symbol.flow-meter-value-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": 0, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "flowRate" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "broken", + "name": "{i18n:scada.symbol.broken}", + "hint": "{i18n:scada.symbol.broken-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.broken}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "displayClick", + "name": "{i18n:scada.symbol.on-display-click}", + "hint": "{i18n:scada.symbol.on-display-click-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.units}", + "type": "units", + "default": "m³/hr", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "medium-width", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultBorderColor", + "name": "{i18n:scada.symbol.default-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#4A4848", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeBorderColor", + "name": "{i18n:scada.symbol.active-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1C943E", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningBorderColor", + "name": "{i18n:scada.symbol.warning-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FAA405", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalBorderColor", + "name": "{i18n:scada.symbol.critical-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#D12730", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FFFFFF", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0m³/hr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg new file mode 100644 index 0000000000..b3f6488f3e --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg @@ -0,0 +1,558 @@ + +{ + "title": "Bottom right elbow pipe", + "description": "Bottom right elbow pipe with fluid and leak visualizations.", + "searchTags": [ + "pipe", + "elbow" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-liquid', 'center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-liquid', 'horizontal-fluid');\n\ncenterLiquidPattern.attr({'xlink:href': '#'+horizontalLiquidPattern.id()});\n\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-liquid', 'vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(-delta, delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", + "tags": [ + { + "tag": "center-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "horizontal-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "vertical-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/bottom-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/bottom-tee-pipe.svg new file mode 100644 index 0000000000..9e44380509 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/bottom-tee-pipe.svg @@ -0,0 +1,854 @@ + +{ + "title": "Bottom tee pipe", + "description": "Bottom tee pipe with configurable left/right/bottom fluid and leak visualizations.", + "searchTags": [ + "pipe", + "tee" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "var leftLiquidPattern = prepareLiquidPattern('left-liquid', 'left-fluid');\nvar rightLiquidPattern = prepareLiquidPattern('right-liquid', 'right-fluid');\nvar bottomLiquidPattern = prepareLiquidPattern('bottom-liquid', 'bottom-fluid');\n\nvar genericLiquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (genericLiquidPattern) {\n genericLiquidPattern.id(ctx.api.generateElementId());\n leftLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n rightLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n bottomLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n}\n\nupdateLiquidPatternAnimation(leftLiquidPattern, 'left');\nupdateLiquidPatternAnimation(rightLiquidPattern, 'right');\nupdateLiquidPatternAnimation(bottomLiquidPattern, 'bottom');\n\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, prefix) {\n if (liquidPattern) {\n var fluid = ctx.values[prefix + 'Fluid'] && !ctx.values.leak;\n var flow = ctx.values[prefix + 'Flow'];\n var flowDirection = ctx.values[prefix + 'FlowDirection'];\n var flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n}\n\n", + "tags": [ + { + "tag": "bottom-fluid", + "stateRenderFunction": "var fluid = ctx.values.bottomFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "bottom-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.bottomFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.bottomFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "left-fluid", + "stateRenderFunction": "var fluid = ctx.values.leftFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "left-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.leftFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.leftFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "overlay", + "stateRenderFunction": "var fluid = (ctx.values.leftFluid || ctx.values.rightFluid ||\n ctx.values.bottomFluid) && !ctx.values.leak;\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "right-fluid", + "stateRenderFunction": "var fluid = ctx.values.rightFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "right-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.rightFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.rightFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "leftFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "leftFluidColor", + "name": "{i18n:scada.symbol.left-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "rightFluidColor", + "name": "{i18n:scada.symbol.right-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "bottomFluidColor", + "name": "{i18n:scada.symbol.bottom-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/centrifugal-pump.svg b/application/src/main/data/json/system/scada_symbols/centrifugal-pump.svg new file mode 100644 index 0000000000..c1bef78aa5 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/centrifugal-pump.svg @@ -0,0 +1,611 @@ + +{ + "title": "Centrifugal pump", + "description": "Centrifugal pump with configurable connectors, running animation and various states.", + "searchTags": [ + "pump", + "centrifugal" + ], + "widgetSizeX": 2, + "widgetSizeY": 2, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.stoppedColor;\nif (ctx.values.critical) {\n color = ctx.properties.criticalColor;\n} else if (ctx.values.warning) {\n color = ctx.properties.warningColor;\n} else if (ctx.values.running) {\n color = ctx.properties.runningColor;\n}\nelement.attr({fill: color});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": null + }, + { + "tag": "center", + "stateRenderFunction": "var running = ctx.values.running;\nvar t = element.timeline();\nif (running) {\n if (!t.active()) {\n if (t.time()) {\n t.play();\n } else {\n ctx.api.animate(element, 2000).ease('-').rotate(360).loop();\n t = element.timeline();\n }\n }\n var speed = ctx.values.rotationAnimationSpeed;\n t.speed(speed);\n} else {\n t.pause();\n}\n", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "leftBottomConnector", + "stateRenderFunction": "if (ctx.properties.leftBottomConnector) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leftTopConnector", + "stateRenderFunction": "if (ctx.properties.leftTopConnector) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "rightBottomConnector", + "stateRenderFunction": "if (ctx.properties.rightBottomConnector) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "rightTopConnector", + "stateRenderFunction": "if (ctx.properties.rightTopConnector) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "topLeftConnector", + "stateRenderFunction": "if (ctx.properties.topLeftConnector) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "topRightConnector", + "stateRenderFunction": "if (ctx.properties.topRightConnector) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "running", + "name": "{i18n:scada.symbol.running}", + "hint": "{i18n:scada.symbol.running-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.running}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rotationAnimationSpeed", + "name": "{i18n:scada.symbol.rotation-animation-speed}", + "hint": "{i18n:scada.symbol.rotation-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + } + ], + "properties": [ + { + "id": "rightTopConnector", + "name": "{i18n:scada.symbol.right-top-connector}", + "type": "switch", + "default": true, + "required": null, + "subLabel": "", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "rightBottomConnector", + "name": "{i18n:scada.symbol.right-bottom-connector}", + "type": "switch", + "default": false, + "required": null, + "subLabel": "", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "leftTopConnector", + "name": "{i18n:scada.symbol.left-top-connector}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "leftBottomConnector", + "name": "{i18n:scada.symbol.left-bottom-connector}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "topLeftConnector", + "name": "{i18n:scada.symbol.top-left-connector}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "topRightConnector", + "name": "{i18n:scada.symbol.top-right-connector}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "runningColor", + "name": "{i18n:scada.symbol.running-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "stoppedColor", + "name": "{i18n:scada.symbol.stopped-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningColor", + "name": "{i18n:scada.symbol.warning-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalColor", + "name": "{i18n:scada.symbol.critical-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/cross-pipe.svg b/application/src/main/data/json/system/scada_symbols/cross-pipe.svg new file mode 100644 index 0000000000..bfed29df3a --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/cross-pipe.svg @@ -0,0 +1,1047 @@ + +{ + "title": "Cross pipe", + "description": "Cross pipe with configurable left/right/top/bottom fluid and leak visualizations.", + "searchTags": [ + "pipe", + "cross" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "var leftLiquidPattern = prepareLiquidPattern('left-liquid', 'left-fluid');\nvar rightLiquidPattern = prepareLiquidPattern('right-liquid', 'right-fluid');\nvar topLiquidPattern = prepareLiquidPattern('top-liquid', 'top-fluid');\nvar bottomLiquidPattern = prepareLiquidPattern('bottom-liquid', 'bottom-fluid');\n\nvar genericLiquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (genericLiquidPattern) {\n genericLiquidPattern.id(ctx.api.generateElementId());\n leftLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n rightLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n topLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n bottomLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n}\n\nupdateLiquidPatternAnimation(leftLiquidPattern, 'left');\nupdateLiquidPatternAnimation(rightLiquidPattern, 'right');\nupdateLiquidPatternAnimation(topLiquidPattern, 'top');\nupdateLiquidPatternAnimation(bottomLiquidPattern, 'bottom');\n\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, prefix) {\n if (liquidPattern) {\n var fluid = ctx.values[prefix + 'Fluid'] && !ctx.values.leak;\n var flow = ctx.values[prefix + 'Flow'];\n var flowDirection = ctx.values[prefix + 'FlowDirection'];\n var flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n}\n\n", + "tags": [ + { + "tag": "bottom-fluid", + "stateRenderFunction": "var fluid = ctx.values.bottomFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "bottom-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.bottomFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.bottomFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "left-fluid", + "stateRenderFunction": "var fluid = ctx.values.leftFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "left-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.leftFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.leftFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "overlay", + "stateRenderFunction": "var fluid = (ctx.values.leftFluid || ctx.values.rightFluid ||\n ctx.values.topFluid || ctx.values.bottomFluid) && !ctx.values.leak;\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "right-fluid", + "stateRenderFunction": "var fluid = ctx.values.rightFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "right-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.rightFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.rightFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-fluid", + "stateRenderFunction": "var fluid = ctx.values.topFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.topFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.topFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "leftFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "leftFluidColor", + "name": "{i18n:scada.symbol.left-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "rightFluidColor", + "name": "{i18n:scada.symbol.right-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "topFluidColor", + "name": "{i18n:scada.symbol.top-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "bottomFluidColor", + "name": "{i18n:scada.symbol.bottom-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-ball-valve.svg b/application/src/main/data/json/system/scada_symbols/horizontal-ball-valve.svg new file mode 100644 index 0000000000..da140b4383 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/horizontal-ball-valve.svg @@ -0,0 +1,271 @@ + +{ + "title": "Horizontal ball valve", + "description": "Horizontal ball valve with open/close animation and state colors.", + "searchTags": [ + "valve", + "ball" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var opened = ctx.values.opened;\nvar openAnimate = element.remember('openAnimate');\nvar color = opened ? ctx.properties.openedColor : ctx.properties.closedColor;\nif (!openAnimate) {\n element.attr({fill: color});\n} else {\n ctx.api.animate(element, 500).attr({fill: color});\n element.remember('openAnimate', false);\n}\n", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": " ctx.tags.wheel.forEach(e => {\n e.remember('openAnimate', true);\n });\n ctx.tags.background.forEach(e => {\n e.remember('openAnimate', true);\n });\n\n\nvar opened = ctx.values.opened;\nvar action = opened ? 'close' : 'open';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('opened', !opened);\n }\n});" + } + } + }, + { + "tag": "wheel", + "stateRenderFunction": "var opened = ctx.values.opened;\nvar openAnimate = element.remember('openAnimate');\nvar angle = opened ? ctx.properties.openedRotationAngle : ctx.properties.closedRotationAngle;\nif (!openAnimate) {\n element.transform({rotate: angle, originX: 100});\n} else {\n ctx.api.animate(element, 500).transform({rotate: angle, originX: 100});\n element.remember('openAnimate', false);\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "opened", + "name": "{i18n:scada.symbol.opened}", + "hint": "{i18n:scada.symbol.opened-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.opened}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "open", + "name": "{i18n:scada.symbol.open}", + "hint": "{i18n:scada.symbol.open-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": true, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "close", + "name": "{i18n:scada.symbol.close}", + "hint": "{i18n:scada.symbol.close-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "openedColor", + "name": "{i18n:scada.symbol.opened-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "closedColor", + "name": "{i18n:scada.symbol.closed-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "openedRotationAngle", + "name": "{i18n:scada.symbol.opened-rotation-angle}", + "type": "number", + "default": 0, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": -179, + "max": 179, + "step": null + }, + { + "id": "closedRotationAngle", + "name": "{i18n:scada.symbol.closed-rotation-angle}", + "type": "number", + "default": 90, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": -179, + "max": 179, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg new file mode 100644 index 0000000000..8b34deaffd --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg @@ -0,0 +1,948 @@ + +{ + "title": "Horizontal inline flow meter", + "description": "Horizontal inline flow meter component used to display flow related value and render various states. Includes pipe fluid and leak visualizations.", + "searchTags": [ + "meter", + "flow meter" + ], + "widgetSizeX": 2, + "widgetSizeY": 1, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.backgroundColor;\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "border", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.defaultBorderColor;\nif (ctx.values.critical) {\n colorProcessor = ctx.properties.criticalBorderColor;\n} else if (ctx.values.warning) {\n colorProcessor = ctx.properties.warningBorderColor;\n} else if (value) {\n colorProcessor = ctx.properties.activeBorderColor;\n}\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "broken", + "stateRenderFunction": "var broken = ctx.values.broken;\nif (broken) {\n element.show();\n} else {\n element.hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "value", + "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "valueUnits", + "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + } + ], + "behavior": [ + { + "id": "value", + "name": "{i18n:scada.symbol.value}", + "hint": "{i18n:scada.symbol.flow-meter-value-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": 0, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "flowRate" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "broken", + "name": "{i18n:scada.symbol.broken}", + "hint": "{i18n:scada.symbol.broken-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.broken}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "displayClick", + "name": "{i18n:scada.symbol.on-display-click}", + "hint": "{i18n:scada.symbol.on-display-click-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.units}", + "type": "units", + "default": "m³/hr", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "medium-width", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultBorderColor", + "name": "{i18n:scada.symbol.default-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#4A4848", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeBorderColor", + "name": "{i18n:scada.symbol.active-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1C943E", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningBorderColor", + "name": "{i18n:scada.symbol.warning-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FAA405", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalBorderColor", + "name": "{i18n:scada.symbol.critical-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#D12730", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FFFFFF", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0m³/hr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-pipe.svg b/application/src/main/data/json/system/scada_symbols/horizontal-pipe.svg new file mode 100644 index 0000000000..d2ddc67657 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/horizontal-pipe.svg @@ -0,0 +1,422 @@ + +{ + "title": "Horizontal pipe", + "description": "Horizontal pipe with fluid and leak visualizations.", + "searchTags": [ + "pipe", + "horizontal pipe" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "tags": [ + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-wheel-valve.svg b/application/src/main/data/json/system/scada_symbols/horizontal-wheel-valve.svg new file mode 100644 index 0000000000..a96f61781d --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/horizontal-wheel-valve.svg @@ -0,0 +1,300 @@ + +{ + "title": "Horizontal wheel valve", + "description": "Horizontal wheel valve with open/close animation and state colors.", + "searchTags": [ + "valve", + "wheel" + ], + "widgetSizeX": 2, + "widgetSizeY": 1, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var opened = ctx.values.opened;\nvar openAnimate = element.remember('openAnimate');\nvar color = opened ? ctx.properties.openedColor : ctx.properties.closedColor;\nif (!openAnimate) {\n element.attr({fill: color});\n} else {\n ctx.api.animate(element, 500).attr({fill: color});\n element.remember('openAnimate', false);\n}\n", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": " ctx.tags.wheel.forEach(e => {\n e.remember('openAnimate', true);\n });\n ctx.tags.background.forEach(e => {\n e.remember('openAnimate', true);\n });\n\n\nvar opened = ctx.values.opened;\nvar action = opened ? 'close' : 'open';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('opened', !opened);\n }\n});" + } + } + }, + { + "tag": "wheel", + "stateRenderFunction": "var opened = ctx.values.opened;\nvar openAnimate = element.remember('openAnimate');\nvar angle = opened ? ctx.properties.openedRotationAngle : ctx.properties.closedRotationAngle;\nif (!openAnimate) {\n element.transform({rotate: angle});\n} else {\n ctx.api.animate(element, 500).transform({rotate: angle});\n element.remember('openAnimate', false);\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "opened", + "name": "{i18n:scada.symbol.opened}", + "hint": "{i18n:scada.symbol.opened-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.opened}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "open", + "name": "{i18n:scada.symbol.open}", + "hint": "{i18n:scada.symbol.open-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": true, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "close", + "name": "{i18n:scada.symbol.close}", + "hint": "{i18n:scada.symbol.close-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "openedColor", + "name": "{i18n:scada.symbol.opened-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "closedColor", + "name": "{i18n:scada.symbol.closed-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "openedRotationAngle", + "name": "{i18n:scada.symbol.opened-rotation-angle}", + "type": "number", + "default": 0, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": -179, + "max": 179, + "step": null + }, + { + "id": "closedRotationAngle", + "name": "{i18n:scada.symbol.closed-rotation-angle}", + "type": "number", + "default": 90, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": -179, + "max": 179, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg new file mode 100644 index 0000000000..cd9b5f1b7a --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg @@ -0,0 +1,558 @@ +{ + "title": "Left bottom elbow pipe", + "description": "Left bottom elbow pipe with fluid and leak visualizations.", + "searchTags": [ + "pipe", + "elbow" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-liquid', 'center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-liquid', 'horizontal-fluid');\n\ncenterLiquidPattern.attr({'xlink:href': '#'+horizontalLiquidPattern.id()});\n\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-liquid', 'vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(-delta, delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", + "tags": [ + { + "tag": "center-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "horizontal-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "vertical-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg new file mode 100644 index 0000000000..2a5c3ce47a --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg @@ -0,0 +1,166 @@ + + { + "title": "Left drain pipe", + "description": "Left drain pipe", + "searchTags": [ + "pipe", + "drain" + ], + "widgetSizeX": 2, + "widgetSizeY": 1, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var color = ctx.properties.fluidColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#8CAA5C", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg new file mode 100644 index 0000000000..2f91847b71 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg @@ -0,0 +1,181 @@ + +{ + "title": "Left elbow drain pipe", + "description": "Left elbow drain pipe", + "searchTags": [ + "pipe", + "drain" + ], + "widgetSizeX": 2, + "widgetSizeY": 1, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var color = ctx.properties.fluidColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#8CAA5C", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg new file mode 100644 index 0000000000..b1da702871 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg @@ -0,0 +1,964 @@ + +{ + "title": "Left flow meter", + "description": "Left flow meter component used to display flow related value and render various states. Includes pipe fluid and leak visualizations.", + "searchTags": [ + "meter", + "flow meter" + ], + "widgetSizeX": 2, + "widgetSizeY": 2, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.backgroundColor;\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "border", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.defaultBorderColor;\nif (ctx.values.critical) {\n colorProcessor = ctx.properties.criticalBorderColor;\n} else if (ctx.values.warning) {\n colorProcessor = ctx.properties.warningBorderColor;\n} else if (value) {\n colorProcessor = ctx.properties.activeBorderColor;\n}\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "broken", + "stateRenderFunction": "var broken = ctx.values.broken;\nif (broken) {\n element.show();\n} else {\n element.hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "value", + "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "valueUnits", + "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + } + ], + "behavior": [ + { + "id": "value", + "name": "{i18n:scada.symbol.value}", + "hint": "{i18n:scada.symbol.flow-meter-value-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": 0, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "flowRate" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "broken", + "name": "{i18n:scada.symbol.broken}", + "hint": "{i18n:scada.symbol.broken-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.broken}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "displayClick", + "name": "{i18n:scada.symbol.on-display-click}", + "hint": "{i18n:scada.symbol.on-display-click-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.units}", + "type": "units", + "default": "m³/hr", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "medium-width", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultBorderColor", + "name": "{i18n:scada.symbol.default-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#4A4848", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeBorderColor", + "name": "{i18n:scada.symbol.active-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1C943E", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningBorderColor", + "name": "{i18n:scada.symbol.warning-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FAA405", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalBorderColor", + "name": "{i18n:scada.symbol.critical-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#D12730", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FFFFFF", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0m³/hr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/left-motor-pump.svg new file mode 100644 index 0000000000..5be0a454e5 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/left-motor-pump.svg @@ -0,0 +1,526 @@ +{ + "title": "Left motor pump", + "description": "Left motor pump with configurable states.", + "searchTags": [ + "motor", + "pump" + ], + "widgetSizeX": 4, + "widgetSizeY": 3, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.stoppedColor;\nif (ctx.values.critical) {\n color = ctx.properties.criticalColor;\n} else if (ctx.values.warning) {\n color = ctx.properties.warningColor;\n} else if (ctx.values.running) {\n color = ctx.properties.runningColor;\n}\nelement.attr({fill: color});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + } + ], + "behavior": [ + { + "id": "running", + "name": "{i18n:scada.symbol.running}", + "hint": "{i18n:scada.symbol.running-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.running}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + } + ], + "properties": [ + { + "id": "runningColor", + "name": "{i18n:scada.symbol.running-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "stoppedColor", + "name": "{i18n:scada.symbol.stopped-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningColor", + "name": "{i18n:scada.symbol.warning-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalColor", + "name": "{i18n:scada.symbol.critical-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg new file mode 100644 index 0000000000..a0e01f639b --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg @@ -0,0 +1,866 @@ + +{ + "title": "Left tee pipe", + "description": "Left tee pipe with configurable left/top/bottom fluid and leak visualizations.", + "searchTags": [ + "pipe", + "tee" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "var leftLiquidPattern = prepareLiquidPattern('left-liquid', 'left-fluid');\nvar topLiquidPattern = prepareLiquidPattern('top-liquid', 'top-fluid');\nvar bottomLiquidPattern = prepareLiquidPattern('bottom-liquid', 'bottom-fluid');\n\nvar genericLiquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (genericLiquidPattern) {\n genericLiquidPattern.id(ctx.api.generateElementId());\n leftLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n topLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n bottomLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n}\n\nupdateLiquidPatternAnimation(leftLiquidPattern, 'left');\nupdateLiquidPatternAnimation(topLiquidPattern, 'top');\nupdateLiquidPatternAnimation(bottomLiquidPattern, 'bottom');\n\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, prefix) {\n if (liquidPattern) {\n var fluid = ctx.values[prefix + 'Fluid'] && !ctx.values.leak;\n var flow = ctx.values[prefix + 'Flow'];\n var flowDirection = ctx.values[prefix + 'FlowDirection'];\n var flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n}\n\n", + "tags": [ + { + "tag": "bottom-fluid", + "stateRenderFunction": "var fluid = ctx.values.bottomFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "bottom-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.bottomFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.bottomFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "left-fluid", + "stateRenderFunction": "var fluid = ctx.values.leftFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "left-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.leftFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.leftFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "overlay", + "stateRenderFunction": "var fluid = (ctx.values.leftFluid ||\n ctx.values.topFluid || ctx.values.bottomFluid) && !ctx.values.leak;\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "top-fluid", + "stateRenderFunction": "var fluid = ctx.values.topFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.topFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.topFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "leftFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "leftFluidColor", + "name": "{i18n:scada.symbol.left-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "topFluidColor", + "name": "{i18n:scada.symbol.top-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "bottomFluidColor", + "name": "{i18n:scada.symbol.bottom-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-top-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-top-elbow-pipe.svg new file mode 100644 index 0000000000..276f3aa004 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/left-top-elbow-pipe.svg @@ -0,0 +1,558 @@ +{ + "title": "Left top elbow pipe", + "description": "Left top elbow pipe with fluid and leak visualizations.", + "searchTags": [ + "pipe", + "elbow" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-liquid', 'center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-liquid', 'horizontal-fluid');\n\ncenterLiquidPattern.attr({'xlink:href': '#'+horizontalLiquidPattern.id()});\n\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-liquid', 'vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(delta, -delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", + "tags": [ + { + "tag": "center-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "horizontal-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "vertical-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg new file mode 100644 index 0000000000..d9defd33d9 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg @@ -0,0 +1,88 @@ + +{ + "title": "Long bottom filter", + "description": "Long bottom filter", + "searchTags": [ + "filter" + ], + "widgetSizeX": 1, + "widgetSizeY": 3, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + } + ], + "properties": [] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg new file mode 100644 index 0000000000..ffc77b0ac0 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg @@ -0,0 +1,487 @@ + +{ + "title": "Long horizontal pipe", + "description": "Long horizontal pipe with fluid and leak visualizations.", + "searchTags": [ + "pipe", + "horizontal pipe" + ], + "widgetSizeX": 2, + "widgetSizeY": 1, + "tags": [ + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-top-filter.svg b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg new file mode 100644 index 0000000000..a63b679f0a --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg @@ -0,0 +1,88 @@ + +{ + "title": "Long top filter", + "description": "Title\nLong top filter\n", + "searchTags": [ + "filter" + ], + "widgetSizeX": 1, + "widgetSizeY": 3, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + } + ], + "properties": [] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg new file mode 100644 index 0000000000..7f4e30edad --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg @@ -0,0 +1,484 @@ + +{ + "title": "Long vertical pipe", + "description": "Long vertical pipe with fluid and leak visualizations.", + "searchTags": [ + "pipe", + "vertical pipe" + ], + "widgetSizeX": 1, + "widgetSizeY": 2, + "tags": [ + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg new file mode 100644 index 0000000000..3fe3501e16 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg @@ -0,0 +1,167 @@ +{ + "title": "Right drain pipe", + "description": "Right drain pipe", + "searchTags": [ + "pipe", + "drain" + ], + "widgetSizeX": 2, + "widgetSizeY": 1, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var color = ctx.properties.fluidColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#8CAA5C", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg new file mode 100644 index 0000000000..14ede98e81 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg @@ -0,0 +1,178 @@ + +{ + "title": "Right elbow drain pipe", + "description": "Right elbow drain pipe", + "searchTags": [ + "pipe", + "drain" + ], + "widgetSizeX": 2, + "widgetSizeY": 1, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var color = ctx.properties.fluidColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#8CAA5C", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg new file mode 100644 index 0000000000..e5df51e561 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg @@ -0,0 +1,964 @@ + +{ + "title": "Right flow meter", + "description": "Right flow meter component used to display flow related value and render various states. Includes pipe fluid and leak visualizations.", + "searchTags": [ + "meter", + "flow meter" + ], + "widgetSizeX": 2, + "widgetSizeY": 2, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.backgroundColor;\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "border", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.defaultBorderColor;\nif (ctx.values.critical) {\n colorProcessor = ctx.properties.criticalBorderColor;\n} else if (ctx.values.warning) {\n colorProcessor = ctx.properties.warningBorderColor;\n} else if (value) {\n colorProcessor = ctx.properties.activeBorderColor;\n}\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "broken", + "stateRenderFunction": "var broken = ctx.values.broken;\nif (broken) {\n element.show();\n} else {\n element.hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "value", + "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "valueUnits", + "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + } + ], + "behavior": [ + { + "id": "value", + "name": "{i18n:scada.symbol.value}", + "hint": "{i18n:scada.symbol.flow-meter-value-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": 0, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "flowRate" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "broken", + "name": "{i18n:scada.symbol.broken}", + "hint": "{i18n:scada.symbol.broken-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.broken}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "displayClick", + "name": "{i18n:scada.symbol.on-display-click}", + "hint": "{i18n:scada.symbol.on-display-click-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.units}", + "type": "units", + "default": "m³/hr", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "medium-width", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultBorderColor", + "name": "{i18n:scada.symbol.default-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#4A4848", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeBorderColor", + "name": "{i18n:scada.symbol.active-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1C943E", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningBorderColor", + "name": "{i18n:scada.symbol.warning-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FAA405", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalBorderColor", + "name": "{i18n:scada.symbol.critical-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#D12730", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FFFFFF", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0m³/hr + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/right-motor-pump.svg new file mode 100644 index 0000000000..7ada9a91e3 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/right-motor-pump.svg @@ -0,0 +1,526 @@ +{ + "title": "Right motor pump", + "description": "Right motor pump with configurable states.", + "searchTags": [ + "motor", + "pump" + ], + "widgetSizeX": 4, + "widgetSizeY": 3, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.stoppedColor;\nif (ctx.values.critical) {\n color = ctx.properties.criticalColor;\n} else if (ctx.values.warning) {\n color = ctx.properties.warningColor;\n} else if (ctx.values.running) {\n color = ctx.properties.runningColor;\n}\nelement.attr({fill: color});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + } + ], + "behavior": [ + { + "id": "running", + "name": "{i18n:scada.symbol.running}", + "hint": "{i18n:scada.symbol.running-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.running}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + } + ], + "properties": [ + { + "id": "runningColor", + "name": "{i18n:scada.symbol.running-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "stoppedColor", + "name": "{i18n:scada.symbol.stopped-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningColor", + "name": "{i18n:scada.symbol.warning-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalColor", + "name": "{i18n:scada.symbol.critical-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg new file mode 100644 index 0000000000..4c84999b1f --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg @@ -0,0 +1,854 @@ + +{ + "title": "Right tee pipe", + "description": "Right tee pipe with configurable right/top/bottom fluid and leak visualizations.", + "searchTags": [ + "pipe", + "tee" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "var rightLiquidPattern = prepareLiquidPattern('right-liquid', 'right-fluid');\nvar topLiquidPattern = prepareLiquidPattern('top-liquid', 'top-fluid');\nvar bottomLiquidPattern = prepareLiquidPattern('bottom-liquid', 'bottom-fluid');\n\nvar genericLiquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (genericLiquidPattern) {\n genericLiquidPattern.id(ctx.api.generateElementId());\n rightLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n topLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n bottomLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n}\n\nupdateLiquidPatternAnimation(rightLiquidPattern, 'right');\nupdateLiquidPatternAnimation(topLiquidPattern, 'top');\nupdateLiquidPatternAnimation(bottomLiquidPattern, 'bottom');\n\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, prefix) {\n if (liquidPattern) {\n var fluid = ctx.values[prefix + 'Fluid'] && !ctx.values.leak;\n var flow = ctx.values[prefix + 'Flow'];\n var flowDirection = ctx.values[prefix + 'FlowDirection'];\n var flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n}\n\n", + "tags": [ + { + "tag": "bottom-fluid", + "stateRenderFunction": "var fluid = ctx.values.bottomFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "bottom-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.bottomFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.bottomFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "overlay", + "stateRenderFunction": "var fluid = (ctx.values.rightFluid ||\n ctx.values.topFluid || ctx.values.bottomFluid) && !ctx.values.leak;\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "right-fluid", + "stateRenderFunction": "var fluid = ctx.values.rightFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "right-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.rightFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.rightFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-fluid", + "stateRenderFunction": "var fluid = ctx.values.topFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.topFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.topFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "rightFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "rightFluidColor", + "name": "{i18n:scada.symbol.right-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "topFluidColor", + "name": "{i18n:scada.symbol.top-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "bottomFluidColor", + "name": "{i18n:scada.symbol.bottom-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg new file mode 100644 index 0000000000..b3235c5906 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg @@ -0,0 +1,88 @@ + +{ + "title": "Short bottom filter", + "description": "Short bottom filter", + "searchTags": [ + "filter" + ], + "widgetSizeX": 1, + "widgetSizeY": 2, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + } + ], + "properties": [] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg new file mode 100644 index 0000000000..ffe74a681e --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg @@ -0,0 +1,160 @@ + + { + "title": "Short left drain pipe", + "description": "Short left drain pipe", + "searchTags": [ + "pipe", + "drain" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var color = ctx.properties.fluidColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#8CAA5C", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg new file mode 100644 index 0000000000..306898e3ae --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg @@ -0,0 +1,160 @@ + + { + "title": "Short right drain pipe", + "description": "Short right drain pipe", + "searchTags": [ + "pipe", + "drain" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var color = ctx.properties.fluidColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#8CAA5C", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/short-top-filter.svg b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg new file mode 100644 index 0000000000..d50eada2aa --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg @@ -0,0 +1,88 @@ + +{ + "title": "Short top filter", + "description": "Short top filter", + "searchTags": [ + "filter" + ], + "widgetSizeX": 1, + "widgetSizeY": 2, + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + } + ], + "behavior": [ + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + } + ], + "properties": [] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/small-left-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/small-left-motor-pump.svg new file mode 100644 index 0000000000..85e617ff22 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/small-left-motor-pump.svg @@ -0,0 +1,428 @@ +{ + "title": "Small left motor pump", + "description": "Small left motor pump with configurable states.", + "searchTags": [ + "motor", + "pump" + ], + "widgetSizeX": 3, + "widgetSizeY": 2, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.stoppedColor;\nif (ctx.values.critical) {\n color = ctx.properties.criticalColor;\n} else if (ctx.values.warning) {\n color = ctx.properties.warningColor;\n} else if (ctx.values.running) {\n color = ctx.properties.runningColor;\n}\nelement.attr({fill: color});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + } + ], + "behavior": [ + { + "id": "running", + "name": "{i18n:scada.symbol.running}", + "hint": "{i18n:scada.symbol.running-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.running}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + } + ], + "properties": [ + { + "id": "runningColor", + "name": "{i18n:scada.symbol.running-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "stoppedColor", + "name": "{i18n:scada.symbol.stopped-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningColor", + "name": "{i18n:scada.symbol.warning-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalColor", + "name": "{i18n:scada.symbol.critical-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/small-right-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/small-right-motor-pump.svg new file mode 100644 index 0000000000..7b9b1c260d --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/small-right-motor-pump.svg @@ -0,0 +1,428 @@ + +{ + "title": "Small right motor pump", + "description": "Small right motor pump with configurable states.", + "searchTags": [ + "motor", + "pump" + ], + "widgetSizeX": 3, + "widgetSizeY": 2, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.stoppedColor;\nif (ctx.values.critical) {\n color = ctx.properties.criticalColor;\n} else if (ctx.values.warning) {\n color = ctx.properties.warningColor;\n} else if (ctx.values.running) {\n color = ctx.properties.runningColor;\n}\nelement.attr({fill: color});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + } + ], + "behavior": [ + { + "id": "running", + "name": "{i18n:scada.symbol.running}", + "hint": "{i18n:scada.symbol.running-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.running}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "running" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + } + ], + "properties": [ + { + "id": "runningColor", + "name": "{i18n:scada.symbol.running-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "stoppedColor", + "name": "{i18n:scada.symbol.stopped-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningColor", + "name": "{i18n:scada.symbol.warning-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalColor", + "name": "{i18n:scada.symbol.critical-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg new file mode 100644 index 0000000000..bac39d98dc --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg @@ -0,0 +1,965 @@ + +{ + "title": "Top flow meter", + "description": "Top flow meter component used to display flow related value and render various states. Includes pipe fluid and leak visualizations.", + "searchTags": [ + "meter", + "flow meter" + ], + "widgetSizeX": 2, + "widgetSizeY": 2, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.backgroundColor;\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "border", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.defaultBorderColor;\nif (ctx.values.critical) {\n colorProcessor = ctx.properties.criticalBorderColor;\n} else if (ctx.values.warning) {\n colorProcessor = ctx.properties.warningBorderColor;\n} else if (value) {\n colorProcessor = ctx.properties.activeBorderColor;\n}\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "broken", + "stateRenderFunction": "var broken = ctx.values.broken;\nif (broken) {\n element.show();\n} else {\n element.hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "value", + "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "valueUnits", + "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + } + ], + "behavior": [ + { + "id": "value", + "name": "{i18n:scada.symbol.value}", + "hint": "{i18n:scada.symbol.flow-meter-value-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": 0, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "flowRate" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "broken", + "name": "{i18n:scada.symbol.broken}", + "hint": "{i18n:scada.symbol.broken-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.broken}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "displayClick", + "name": "{i18n:scada.symbol.on-display-click}", + "hint": "{i18n:scada.symbol.on-display-click-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.units}", + "type": "units", + "default": "m³/hr", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "medium-width", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultBorderColor", + "name": "{i18n:scada.symbol.default-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#4A4848", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeBorderColor", + "name": "{i18n:scada.symbol.active-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1C943E", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningBorderColor", + "name": "{i18n:scada.symbol.warning-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FAA405", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalBorderColor", + "name": "{i18n:scada.symbol.critical-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#D12730", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FFFFFF", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0m³/hr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg new file mode 100644 index 0000000000..5537c23bb6 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg @@ -0,0 +1,558 @@ +{ + "title": "Top right elbow pipe", + "description": "Top right elbow pipe with fluid and leak visualizations.", + "searchTags": [ + "pipe", + "elbow" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "var centerLiquidPattern = prepareLiquidPattern('center-liquid', 'center-fluid-background');\nvar horizontalLiquidPattern = prepareLiquidPattern('horizontal-liquid', 'horizontal-fluid');\n\ncenterLiquidPattern.attr({'xlink:href': '#'+horizontalLiquidPattern.id()});\n\nvar verticalLiquidPattern = prepareLiquidPattern('vertical-liquid', 'vertical-fluid');\n\nvar fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\nvar flowAnimationSpeed = ctx.values.flowAnimationSpeed;\n \nif (horizontalLiquidPattern) {\n updateLiquidPatternAnimation(horizontalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (verticalLiquidPattern) {\n updateLiquidPatternAnimation(verticalLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, false);\n}\n\nif (centerLiquidPattern) {\n updateLiquidPatternAnimation(centerLiquidPattern, fluid, \n flow, flowDirection, flowAnimationSpeed, true);\n}\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, fluid, flow, flowDirection, flowAnimationSpeed, center) {\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection, center);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse, center) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n if (center) {\n var delta = deltaX * 1.17 * Math.cos(45*Math.PI/180);\n liquidPattern.animate(1000).ease('-').relative(delta, -delta).loop();\n } else {\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}\n\n", + "tags": [ + { + "tag": "center-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "horizontal-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "vertical-fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg new file mode 100644 index 0000000000..1dfa27e706 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg @@ -0,0 +1,854 @@ + +{ + "title": "Top tee pipe", + "description": "Top tee pipe with configurable left/right/top fluid and leak visualizations.", + "searchTags": [ + "pipe", + "tee" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "var leftLiquidPattern = prepareLiquidPattern('left-liquid', 'left-fluid');\nvar rightLiquidPattern = prepareLiquidPattern('right-liquid', 'right-fluid');\nvar topLiquidPattern = prepareLiquidPattern('top-liquid', 'top-fluid');\n\nvar genericLiquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (genericLiquidPattern) {\n genericLiquidPattern.id(ctx.api.generateElementId());\n leftLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n rightLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n topLiquidPattern.attr({'xlink:href': '#'+genericLiquidPattern.id()});\n}\n\nupdateLiquidPatternAnimation(leftLiquidPattern, 'left');\nupdateLiquidPatternAnimation(rightLiquidPattern, 'right');\nupdateLiquidPatternAnimation(topLiquidPattern, 'top');\n\n\nfunction prepareLiquidPattern(patternId, fluidElementTag) {\n var liquidPattern = ctx.svg.defs().findOne('pattern#'+patternId);\n var fluidElements = ctx.tags[fluidElementTag];\n\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n for (var i=0;i<fluidElements.length;i++) {\n fluidElements[i].fill(liquidPattern);\n }\n } else {\n liquidPattern = fluidElements[0].reference('fill');\n }\n return liquidPattern;\n}\n\nfunction updateLiquidPatternAnimation(liquidPattern, prefix) {\n if (liquidPattern) {\n var fluid = ctx.values[prefix + 'Fluid'] && !ctx.values.leak;\n var flow = ctx.values[prefix + 'Flow'];\n var flowDirection = ctx.values[prefix + 'FlowDirection'];\n var flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n var elementFluid = liquidPattern.remember('fluid');\n var elementFlow = null;\n var elementFlowDirection = null;\n \n if (fluid !== elementFluid) {\n liquidPattern.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n } else {\n elementFlow = liquidPattern.remember('flow');\n elementFlowDirection = liquidPattern.remember('flowDirection');\n }\n \n if (fluid) {\n if (flow !== elementFlow) {\n liquidPattern.remember('flow', flow);\n if (flow) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n liquidPattern.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(flowAnimationSpeed);\n }\n } else {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n}\n\n", + "tags": [ + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "left-fluid", + "stateRenderFunction": "var fluid = ctx.values.leftFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "left-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.leftFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.leftFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "overlay", + "stateRenderFunction": "var fluid = (ctx.values.leftFluid || ctx.values.rightFluid ||\n ctx.values.topFluid) && !ctx.values.leak;\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "right-fluid", + "stateRenderFunction": "var fluid = ctx.values.rightFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "right-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.rightFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.rightFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-fluid", + "stateRenderFunction": "var fluid = ctx.values.topFluid && !ctx.values.leak;\n\nif (fluid) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-fluid-background", + "stateRenderFunction": "var fluid = ctx.values.topFluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.topFluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "leftFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "leftFluidColor", + "name": "{i18n:scada.symbol.left-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "rightFluidColor", + "name": "{i18n:scada.symbol.right-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "topFluidColor", + "name": "{i18n:scada.symbol.top-fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg b/application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg new file mode 100644 index 0000000000..3aee266486 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg @@ -0,0 +1,271 @@ + +{ + "title": "Vertical ball valve", + "description": "Vertical ball valve with open/close animation and state colors.", + "searchTags": [ + "valve", + "ball" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var opened = ctx.values.opened;\nvar openAnimate = element.remember('openAnimate');\nvar color = opened ? ctx.properties.openedColor : ctx.properties.closedColor;\nif (!openAnimate) {\n element.attr({fill: color});\n} else {\n ctx.api.animate(element, 500).attr({fill: color});\n element.remember('openAnimate', false);\n}\n", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": " ctx.tags.wheel.forEach(e => {\n e.remember('openAnimate', true);\n });\n ctx.tags.background.forEach(e => {\n e.remember('openAnimate', true);\n });\n\n\nvar opened = ctx.values.opened;\nvar action = opened ? 'close' : 'open';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('opened', !opened);\n }\n});" + } + } + }, + { + "tag": "wheel", + "stateRenderFunction": "var opened = ctx.values.opened;\nvar openAnimate = element.remember('openAnimate');\nvar angle = opened ? ctx.properties.openedRotationAngle : ctx.properties.closedRotationAngle;\nif (!openAnimate) {\n element.transform({rotate: angle, originY: 100});\n} else {\n ctx.api.animate(element, 500).transform({rotate: angle, originY: 100});\n element.remember('openAnimate', false);\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "opened", + "name": "{i18n:scada.symbol.opened}", + "hint": "{i18n:scada.symbol.opened-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.opened}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "open", + "name": "{i18n:scada.symbol.open}", + "hint": "{i18n:scada.symbol.open-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "close", + "name": "{i18n:scada.symbol.close}", + "hint": "{i18n:scada.symbol.close-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": true, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "openedColor", + "name": "{i18n:scada.symbol.opened-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "closedColor", + "name": "{i18n:scada.symbol.closed-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "openedRotationAngle", + "name": "{i18n:scada.symbol.opened-rotation-angle}", + "type": "number", + "default": 0, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": -179, + "max": 179, + "step": null + }, + { + "id": "closedRotationAngle", + "name": "{i18n:scada.symbol.closed-rotation-angle}", + "type": "number", + "default": 90, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": -179, + "max": 179, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg new file mode 100644 index 0000000000..1c22c8d117 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg @@ -0,0 +1,949 @@ + +{ + "title": "Vertical inline flow meter", + "description": "Vertical inline flow meter component used to display flow related value and render various states. Includes pipe fluid and leak visualizations.", + "searchTags": [ + "meter", + "flow meter" + ], + "widgetSizeX": 1, + "widgetSizeY": 2, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.backgroundColor;\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "border", + "stateRenderFunction": "var value = ctx.values.value;\nvar colorProcessor = ctx.properties.defaultBorderColor;\nif (ctx.values.critical) {\n colorProcessor = ctx.properties.criticalBorderColor;\n} else if (ctx.values.warning) {\n colorProcessor = ctx.properties.warningBorderColor;\n} else if (value) {\n colorProcessor = ctx.properties.activeBorderColor;\n}\ncolorProcessor.update(value);\nvar fill = colorProcessor.color;\nelement.attr({fill: fill});\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "broken", + "stateRenderFunction": "var broken = ctx.values.broken;\nif (broken) {\n element.show();\n} else {\n element.hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "value", + "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + }, + { + "tag": "valueUnits", + "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'displayClick');" + } + } + } + ], + "behavior": [ + { + "id": "value", + "name": "{i18n:scada.symbol.value}", + "hint": "{i18n:scada.symbol.flow-meter-value-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": 0, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "flowRate" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "broken", + "name": "{i18n:scada.symbol.broken}", + "hint": "{i18n:scada.symbol.broken-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.broken}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "displayClick", + "name": "{i18n:scada.symbol.on-display-click}", + "hint": "{i18n:scada.symbol.on-display-click-hint}", + "group": "{i18n:scada.symbol.display}", + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "openInSeparateDialog": false, + "openInPopover": false + } + }, + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": "{i18n:scada.symbol.pipe}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.units}", + "type": "units", + "default": "m³/hr", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "medium-width", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultBorderColor", + "name": "{i18n:scada.symbol.default-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#4A4848", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "activeBorderColor", + "name": "{i18n:scada.symbol.active-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1C943E", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "warningBorderColor", + "name": "{i18n:scada.symbol.warning-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FAA405", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "criticalBorderColor", + "name": "{i18n:scada.symbol.critical-border-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#D12730", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#FFFFFF", + "gradient": { + "advancedMode": false, + "gradient": [ + "rgba(0, 255, 0, 1)", + "rgba(255, 0, 0, 1)" + ], + "gradientAdvanced": [ + { + "source": { + "type": "constant" + }, + "color": "rgba(0, 255, 0, 1)" + }, + { + "source": { + "type": "constant" + }, + "color": "rgba(255, 0, 0, 1)" + } + ], + "minValue": 0, + "maxValue": 100 + }, + "rangeList": { + "advancedMode": false, + "range": [], + "rangeAdvanced": [] + }, + "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0m³/hr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg b/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg new file mode 100644 index 0000000000..18ea6837e0 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg @@ -0,0 +1,422 @@ + +{ + "title": "Vertical pipe", + "description": "Vertical pipe with fluid and leak visualizations.", + "searchTags": [ + "pipe", + "vertical pipe" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "tags": [ + { + "tag": "fluid", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nvar flow = ctx.values.flow;\nvar flowDirection = ctx.values.flowDirection;\n\nvar elementFluid = element.remember('fluid');\nvar elementFlow = null;\nvar elementFlowDirection = null;\n\nif (fluid !== elementFluid) {\n element.remember('fluid', fluid);\n elementFlow = null;\n elementFlowDirection = null;\n} else {\n elementFlow = element.remember('flow');\n elementFlowDirection = element.remember('flowDirection');\n}\n\nvar liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\nif (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n} else {\n liquidPattern = element.reference('fill');\n}\n\nif (fluid) {\n element.show();\n if (flow !== elementFlow) {\n element.remember('flow', flow);\n if (flow) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n } else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n }\n } else if (flow && elementFlowDirection !== flowDirection) {\n element.remember('flowDirection', flowDirection);\n animateFlow(liquidPattern, flowDirection);\n }\n if (flow && liquidPattern) {\n liquidPattern.timeline().speed(ctx.values.flowAnimationSpeed);\n }\n} else {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n }\n element.hide();\n}\n\nfunction animateFlow(liquidPattern, forwardElseReverse) {\n if (liquidPattern) {\n ctx.api.resetAnimation(liquidPattern);\n var deltaX = forwardElseReverse ? 172 : -172;\n liquidPattern.animate(1000).ease('-').relative(deltaX, 0).loop();\n }\n}", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "var fluid = ctx.values.fluid && !ctx.values.leak;\nif (fluid) {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color});\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "leak", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.show();\n} else {\n element.hide();\n}", + "actions": null + }, + { + "tag": "pipe-background", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + } + ], + "behavior": [ + { + "id": "fluid", + "name": "{i18n:scada.symbol.fluid-presence}", + "hint": "{i18n:scada.symbol.fluid-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flow", + "name": "{i18n:scada.symbol.flow-presence}", + "hint": "{i18n:scada.symbol.flow-presence-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.leak-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg new file mode 100644 index 0000000000..57d4d1978a --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg @@ -0,0 +1,1394 @@ + +{ + "title": "Vertical tank", + "description": "Vertical tank with current volume value and level visualizations.", + "searchTags": [ + "tank" + ], + "widgetSizeX": 3, + "widgetSizeY": 5, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var color = ctx.properties.tankColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "fluid", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var liquidPattern = ctx.svg.defs().findOne('pattern#liquid');\n if (liquidPattern) {\n liquidPattern.id(ctx.api.generateElementId());\n element.fill(liquidPattern);\n } else {\n liquidPattern = element.reference('fill');\n }\n\n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n liquidPattern.transform({translateY: 590});\n }\n\n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*765; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n liquidPattern.animate(500)\n .transform({ translateY: 590 + height });\n }\n}\n", + "actions": null + }, + { + "tag": "fluid-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var color = ctx.properties.fluidColor;\n element.attr({fill: color, 'fill-opacity': 1});\n \n var valueSet = element.remember('valueSet');\n if (!valueSet) {\n element.remember('valueSet', true);\n element.attr({height: 0});\n }\n \n var currentVolume = ctx.values.currentVolume; \n var tankCapacity = ctx.values.tankCapacity; \n\n var height = currentVolume / tankCapacity;\n height = Math.max(0, Math.min(1, height))*765; \n \n var elementHeight = element.remember('height');\n if (height !== elementHeight) {\n element.remember('height', height);\n ctx.api.animate(element, 500).attr({height: height});\n }\n}\n", + "actions": null + }, + { + "tag": "scale", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', 'dominant-baseline': 'middle', class: 'majorTickText'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "actions": null + }, + { + "tag": "scale-background", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "top-layer", + "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box", + "stateRenderFunction": "if (!ctx.properties.valueBox) {\n element.hide();\n}", + "actions": null + }, + { + "tag": "value-box-background", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var color = ctx.properties.valueBoxColor;\n element.attr({fill: color});\n}", + "actions": null + }, + { + "tag": "value-text", + "stateRenderFunction": "if (ctx.properties.valueBox) {\n var valueTextFont = ctx.properties.valueTextFont;\n var valueTextColor = ctx.properties.valueTextColor;\n var currentVolume = ctx.values.currentVolume;\n var valueText = ctx.api.formatValue(currentVolume, 0, ctx.properties.valueUnits, false);\n ctx.api.font(element, valueTextFont, valueTextColor);\n ctx.api.text(element, valueText);\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "tankCapacity", + "name": "{i18n:scada.symbol.tank-capacity}", + "hint": "{i18n:scada.symbol.tank-capacity-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SERVER_SCOPE", + "key": "tankCapacity" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "currentVolume", + "name": "{i18n:scada.symbol.current-volume}", + "hint": "{i18n:scada.symbol.current-volume-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": null, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "liquidVolume" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "warning", + "name": "{i18n:scada.symbol.warning-state}", + "hint": "{i18n:scada.symbol.warning-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.warning}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "critical", + "name": "{i18n:scada.symbol.critical-state}", + "hint": "{i18n:scada.symbol.critical-state-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.critical}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "tankColor", + "name": "{i18n:scada.symbol.tank-color}", + "type": "color", + "default": "#E5E5E5", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "fluidColor", + "name": "{i18n:scada.symbol.fluid-color}", + "type": "color", + "default": "#1EC1F480", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBox", + "name": "{i18n:scada.symbol.value-box}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueBoxColor", + "name": "{i18n:scada.symbol.value-box}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueUnits", + "name": "{i18n:scada.symbol.value-text}", + "type": "units", + "default": "gal", + "required": null, + "subLabel": "{i18n:scada.symbol.units}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextFont", + "name": "{i18n:scada.symbol.value-text}", + "type": "font", + "default": { + "size": 36, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "valueTextColor", + "name": "{i18n:scada.symbol.value-text}", + "type": "color", + "default": "#0000008A", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "valueBox", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "scale", + "name": "{i18n:scada.symbol.scale}", + "type": "switch", + "default": true, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "transparent", + "name": "{i18n:scada.symbol.transparent-mode}", + "type": "switch", + "default": false, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorIntervals", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "number", + "default": 10, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": 1 + }, + { + "id": "majorFont", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "font", + "default": { + "size": 24, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal" + }, + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#00000061", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorWarningColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "majorCriticalColor", + "name": "{i18n:scada.symbol.major-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorIntervals", + "name": "{i18n:scada.symbol.minor-ticks}", + "type": "number", + "default": 5, + "required": null, + "subLabel": "{i18n:scada.symbol.intervals}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": 1, + "max": null, + "step": null + }, + { + "id": "minorColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#0000001F", + "required": null, + "subLabel": "{i18n:scada.symbol.normal}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorWarningColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#FAA405", + "required": null, + "subLabel": "{i18n:scada.symbol.warning}", + "divider": true, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "minorCriticalColor", + "name": "{i18n:scada.symbol.minor-ticks-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": "{i18n:scada.symbol.critical}", + "divider": null, + "fieldSuffix": null, + "disableOnProperty": "scale", + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1660 gal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/vertical-wheel-valve.svg b/application/src/main/data/json/system/scada_symbols/vertical-wheel-valve.svg new file mode 100644 index 0000000000..d43083040f --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/vertical-wheel-valve.svg @@ -0,0 +1,300 @@ +{ + "title": "Vertical wheel valve", + "description": "Vertical wheel valve with open/close animation and state colors.", + "searchTags": [ + "valve", + "wheel" + ], + "widgetSizeX": 1, + "widgetSizeY": 2, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var opened = ctx.values.opened;\nvar openAnimate = element.remember('openAnimate');\nvar color = opened ? ctx.properties.openedColor : ctx.properties.closedColor;\nif (!openAnimate) {\n element.attr({fill: color});\n} else {\n ctx.api.animate(element, 500).attr({fill: color});\n element.remember('openAnimate', false);\n}\n", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": " ctx.tags.wheel.forEach(e => {\n e.remember('openAnimate', true);\n });\n ctx.tags.background.forEach(e => {\n e.remember('openAnimate', true);\n });\n\n\nvar opened = ctx.values.opened;\nvar action = opened ? 'close' : 'open';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('opened', !opened);\n }\n});" + } + } + }, + { + "tag": "wheel", + "stateRenderFunction": "var opened = ctx.values.opened;\nvar openAnimate = element.remember('openAnimate');\nvar angle = opened ? ctx.properties.openedRotationAngle : ctx.properties.closedRotationAngle;\nif (!openAnimate) {\n element.transform({rotate: angle});\n} else {\n ctx.api.animate(element, 500).transform({rotate: angle});\n element.remember('openAnimate', false);\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "opened", + "name": "{i18n:scada.symbol.opened}", + "hint": "{i18n:scada.symbol.opened-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.opened}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "open", + "name": "{i18n:scada.symbol.open}", + "hint": "{i18n:scada.symbol.open-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": true, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "close", + "name": "{i18n:scada.symbol.close}", + "hint": "{i18n:scada.symbol.close-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "openedColor", + "name": "{i18n:scada.symbol.opened-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "closedColor", + "name": "{i18n:scada.symbol.closed-color}", + "type": "color", + "default": "#696969", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "openedRotationAngle", + "name": "{i18n:scada.symbol.opened-rotation-angle}", + "type": "number", + "default": 0, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": -179, + "max": 179, + "step": null + }, + { + "id": "closedRotationAngle", + "name": "{i18n:scada.symbol.closed-rotation-angle}", + "type": "number", + "default": 90, + "required": true, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": -179, + "max": 179, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 new file mode 100644 index 0000000000..f1fbfbc297 --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/scada_symbols.json @@ -0,0 +1,14 @@ +{ + "widgetsBundle": { + "alias": "scada_symbols", + "title": "SCADA symbols", + "scada": true, + "image": null, + "description": "Bundle with SCADA symbols", + "order": 9200, + "name": "SCADA symbols" + }, + "widgetTypeFqns": [ + "scada_symbol" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json new file mode 100644 index 0000000000..224938c019 --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json @@ -0,0 +1,52 @@ +{ + "widgetsBundle": { + "alias": "scada_water_system_symbols", + "title": "SCADA water system symbols", + "scada": true, + "image": null, + "description": "Bundle with SCADA symbols for water system", + "order": 9300, + "name": "SCADA water system symbols" + }, + "widgetTypeFqns": [ + "horizontal_pipe", + "long_horizontal_pipe", + "vertical_pipe", + "long_vertical_pipe", + "left_bottom_elbow_pipe", + "bottom_right_elbow_pipe", + "top_right_elbow_pipe", + "left_top_elbow_pipe", + "cross_pipe", + "left_tee_pipe", + "bottom_tee_pipe", + "right_tee_pipe", + "top_tee_pipe", + "right_elbow_drain_pipe", + "left_elbow_drain_pipe", + "left_drain_pipe", + "right_drain_pipe", + "short_left_drain_pipe", + "short_right_drain_pipe", + "top_flow_meter", + "right_flow_meter", + "bottom_flow_meter", + "left_flow_meter", + "horizontal_inline_flow_meter", + "vertical_inline_flow_meter", + "centrifugal_pump", + "small_right_motor_pump", + "small_left_motor_pump", + "right_motor_pump", + "left_motor_pump", + "short_bottom_filter", + "long_bottom_filter", + "short_top_filter", + "long_top_filter", + "horizontal_wheel_valve", + "vertical_wheel_valve", + "horizontal_ball_valve", + "vertical_ball_valve", + "vertical_tank" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/power_button.json b/application/src/main/data/json/system/widget_types/power_button.json index 821873f0b4..30a3facd7b 100644 --- a/application/src/main/data/json/system/widget_types/power_button.json +++ b/application/src/main/data/json/system/widget_types/power_button.json @@ -2,7 +2,7 @@ "fqn": "power_button", "name": "Power button", "deprecated": false, - "image": "tb-image:cG93ZXItYnV0dG9uLnN2Zw==:IlBvd2VyIGJ1dHRvbiIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2lfNDU4OF84ODQyMCkiPgo8cGF0aCBkPSJNMTQ2LjUgODBDMTQ2LjUgMTA1LjEyOSAxMjUuNjgxIDEyNS41IDEwMCAxMjUuNUM3NC4zMTg4IDEyNS41IDUzLjUgMTA1LjEyOSA1My41IDgwQzUzLjUgNTQuODcxIDc0LjMxODggMzQuNSAxMDAgMzQuNUMxMjUuNjgxIDM0LjUgMTQ2LjUgNTQuODcxIDE0Ni41IDgwWiIgZmlsbD0iIzNGNTJERCIvPgo8L2c+CjxwYXRoIGQ9Ik0xNDUuNSA4MEMxNDUuNSAxMDQuNTU2IDEyNS4xNSAxMjQuNSAxMDAgMTI0LjVDNzQuODUwNCAxMjQuNSA1NC41IDEwNC41NTYgNTQuNSA4MEM1NC41IDU1LjQ0MzcgNzQuODUwNCAzNS41IDEwMCAzNS41QzEyNS4xNSAzNS41IDE0NS41IDU1LjQ0MzcgMTQ1LjUgODBaIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTg4Xzg4NDIwKSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAwIDEzNS41QzEzMS4yMDQgMTM1LjUgMTU2LjUgMTEwLjY1MiAxNTYuNSA4MEMxNTYuNSA0OS4zNDgyIDEzMS4yMDQgMjQuNSAxMDAgMjQuNUM2OC43OTU5IDI0LjUgNDMuNSA0OS4zNDgyIDQzLjUgODBDNDMuNSAxMTAuNjUyIDY4Ljc5NTkgMTM1LjUgMTAwIDEzNS41Wk0xMDAgMTI1LjVDMTI1LjY4MSAxMjUuNSAxNDYuNSAxMDUuMTI5IDE0Ni41IDgwQzE0Ni41IDU0Ljg3MSAxMjUuNjgxIDM0LjUgMTAwIDM0LjVDNzQuMzE4OCAzNC41IDUzLjUgNTQuODcxIDUzLjUgODBDNTMuNSAxMDUuMTI5IDc0LjMxODggMTI1LjUgMTAwIDEyNS41WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzQ1ODhfODg0MjApIi8+CjwvZz4KPHBhdGggZD0iTTk4LjM1NzQgNzkuMTg1NVY4MC4xNzM4Qzk4LjM1NzQgODEuMzQ4MyA5OC4yMTA2IDgyLjQwMSA5Ny45MTcgODMuMzMyQzk3LjYyMzQgODQuMjYzIDk3LjIwMDggODUuMDU0NCA5Ni42NDk0IDg1LjcwNjFDOTYuMDk4IDg2LjM1NzcgOTUuNDM1NSA4Ni44NTU1IDk0LjY2MjEgODcuMTk5MkM5My44OTU4IDg3LjU0MyA5My4wMzY1IDg3LjcxNDggOTIuMDg0IDg3LjcxNDhDOTEuMTYwMiA4Ny43MTQ4IDkwLjMxMTUgODcuNTQzIDg5LjUzODEgODcuMTk5MkM4OC43NzE4IDg2Ljg1NTUgODguMTA1OCA4Ni4zNTc3IDg3LjU0IDg1LjcwNjFDODYuOTgxNCA4NS4wNTQ0IDg2LjU0ODIgODQuMjYzIDg2LjI0MDIgODMuMzMyQzg1LjkzMjMgODIuNDAxIDg1Ljc3ODMgODEuMzQ4MyA4NS43NzgzIDgwLjE3MzhWNzkuMTg1NUM4NS43NzgzIDc4LjAxMTEgODUuOTI4NyA3Ni45NjE5IDg2LjIyOTUgNzYuMDM4MUM4Ni41Mzc0IDc1LjEwNzEgODYuOTcwNyA3NC4zMTU4IDg3LjUyOTMgNzMuNjY0MUM4OC4wODc5IDczLjAwNTIgODguNzUwMyA3Mi41MDM5IDg5LjUxNjYgNzIuMTYwMkM5MC4yOSA3MS44MTY0IDkxLjEzODcgNzEuNjQ0NSA5Mi4wNjI1IDcxLjY0NDVDOTMuMDE1IDcxLjY0NDUgOTMuODc0MyA3MS44MTY0IDk0LjY0MDYgNzIuMTYwMkM5NS40MTQxIDcyLjUwMzkgOTYuMDc2NSA3My4wMDUyIDk2LjYyNzkgNzMuNjY0MUM5Ny4xODY1IDc0LjMxNTggOTcuNjEyNiA3NS4xMDcxIDk3LjkwNjIgNzYuMDM4MUM5OC4yMDcgNzYuOTYxOSA5OC4zNTc0IDc4LjAxMTEgOTguMzU3NCA3OS4xODU1Wk05Ni4zMDU3IDgwLjE3MzhWNzkuMTY0MUM5Ni4zMDU3IDc4LjIzMzEgOTYuMjA5IDc3LjQwOTUgOTYuMDE1NiA3Ni42OTM0Qzk1LjgyOTQgNzUuOTc3MiA5NS41NTM3IDc1LjM3NTcgOTUuMTg4NSA3NC44ODg3Qzk0LjgyMzIgNzQuNDAxNyA5NC4zNzU3IDc0LjAzMjkgOTMuODQ1NyA3My43ODIyQzkzLjMyMjkgNzMuNTMxNiA5Mi43Mjg1IDczLjQwNjIgOTIuMDYyNSA3My40MDYyQzkxLjQxOCA3My40MDYyIDkwLjgzNDMgNzMuNTMxNiA5MC4zMTE1IDczLjc4MjJDODkuNzk1OSA3NC4wMzI5IDg5LjM1MTkgNzQuNDAxNyA4OC45Nzk1IDc0Ljg4ODdDODguNjE0MyA3NS4zNzU3IDg4LjMzMTQgNzUuOTc3MiA4OC4xMzA5IDc2LjY5MzRDODcuOTMwMyA3Ny40MDk1IDg3LjgzMDEgNzguMjMzMSA4Ny44MzAxIDc5LjE2NDFWODAuMTczOEM4Ny44MzAxIDgxLjExMiA4Ny45MzAzIDgxLjk0MjcgODguMTMwOSA4Mi42NjZDODguMzMxNCA4My4zODIyIDg4LjYxNzggODMuOTg3MyA4OC45OTAyIDg0LjQ4MTRDODkuMzY5OCA4NC45Njg0IDg5LjgxNzQgODUuMzM3MiA5MC4zMzMgODUuNTg3OUM5MC44NTU4IDg1LjgzODUgOTEuNDM5NSA4NS45NjM5IDkyLjA4NCA4NS45NjM5QzkyLjc1NzIgODUuOTYzOSA5My4zNTUxIDg1LjgzODUgOTMuODc3OSA4NS41ODc5Qzk0LjQwMDcgODUuMzM3MiA5NC44NDExIDg0Ljk2ODQgOTUuMTk5MiA4NC40ODE0Qzk1LjU2NDUgODMuOTg3MyA5NS44NDAyIDgzLjM4MjIgOTYuMDI2NCA4Mi42NjZDOTYuMjEyNiA4MS45NDI3IDk2LjMwNTcgODEuMTEyIDk2LjMwNTcgODAuMTczOFpNMTEzLjQ5MyA3MS44NTk0Vjg3LjVIMTExLjQwOUwxMDMuNTM1IDc1LjQzNjVWODcuNUgxMDEuNDYyVjcxLjg1OTRIMTAzLjUzNUwxMTEuNDQxIDgzLjk1NTFWNzEuODU5NEgxMTMuNDkzWiIgZmlsbD0id2hpdGUiLz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfaV80NTg4Xzg4NDIwIiB4PSI0OC41IiB5PSIzNC41IiB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeD0iLTUiIGR5PSI1Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0iYXJpdGhtZXRpYyIgazI9Ii0xIiBrMz0iMSIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJzaGFwZSIgcmVzdWx0PSJlZmZlY3QxX2lubmVyU2hhZG93XzQ1ODhfODg0MjAiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTg4Xzg4NDIwIiB4PSIzNS41IiB5PSIyMC41IiB3aWR0aD0iMTI5IiBoZWlnaHQ9IjEyNyIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSI0Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA4IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDU4OF84ODQyMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTg4Xzg4NDIwIiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNDU4OF84ODQyMCIgeDE9IjY1Ljg5NjQiIHkxPSIxMjQuNSIgeDI9IjEyOS41MTkiIHkyPSIzMy45MjQ4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDQ0NDQ0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=", + "image": "tb-image:cG93ZXItYnV0dG9uLnN2Zw==:IlBvd2VyIGJ1dHRvbiIgc3lzdGVtIHdpZGdldCBpbWFnZQ==:SU1BR0U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2lfNDU4OF84ODQyMCkiPgo8cGF0aCBkPSJNMTQ2LjUgODBDMTQ2LjUgMTA1LjEyOSAxMjUuNjgxIDEyNS41IDEwMCAxMjUuNUM3NC4zMTg4IDEyNS41IDUzLjUgMTA1LjEyOSA1My41IDgwQzUzLjUgNTQuODcxIDc0LjMxODggMzQuNSAxMDAgMzQuNUMxMjUuNjgxIDM0LjUgMTQ2LjUgNTQuODcxIDE0Ni41IDgwWiIgZmlsbD0iIzNGNTJERCIvPgo8L2c+CjxwYXRoIGQ9Ik0xNDUuNSA4MEMxNDUuNSAxMDQuNTU2IDEyNS4xNSAxMjQuNSAxMDAgMTI0LjVDNzQuODUwNCAxMjQuNSA1NC41IDEwNC41NTYgNTQuNSA4MEM1NC41IDU1LjQ0MzcgNzQuODUwNCAzNS41IDEwMCAzNS41QzEyNS4xNSAzNS41IDE0NS41IDU1LjQ0MzcgMTQ1LjUgODBaIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTg4Xzg4NDIwKSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAwIDEzNS41QzEzMS4yMDQgMTM1LjUgMTU2LjUgMTEwLjY1MiAxNTYuNSA4MEMxNTYuNSA0OS4zNDgyIDEzMS4yMDQgMjQuNSAxMDAgMjQuNUM2OC43OTU5IDI0LjUgNDMuNSA0OS4zNDgyIDQzLjUgODBDNDMuNSAxMTAuNjUyIDY4Ljc5NTkgMTM1LjUgMTAwIDEzNS41Wk0xMDAgMTI1LjVDMTI1LjY4MSAxMjUuNSAxNDYuNSAxMDUuMTI5IDE0Ni41IDgwQzE0Ni41IDU0Ljg3MSAxMjUuNjgxIDM0LjUgMTAwIDM0LjVDNzQuMzE4OCAzNC41IDUzLjUgNTQuODcxIDUzLjUgODBDNTMuNSAxMDUuMTI5IDc0LjMxODggMTI1LjUgMTAwIDEyNS41WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzQ1ODhfODg0MjApIi8+CjwvZz4KPHBhdGggZD0iTTk4LjM1NzQgNzkuMTg1NVY4MC4xNzM4Qzk4LjM1NzQgODEuMzQ4MyA5OC4yMTA2IDgyLjQwMSA5Ny45MTcgODMuMzMyQzk3LjYyMzQgODQuMjYzIDk3LjIwMDggODUuMDU0NCA5Ni42NDk0IDg1LjcwNjFDOTYuMDk4IDg2LjM1NzcgOTUuNDM1NSA4Ni44NTU1IDk0LjY2MjEgODcuMTk5MkM5My44OTU4IDg3LjU0MyA5My4wMzY1IDg3LjcxNDggOTIuMDg0IDg3LjcxNDhDOTEuMTYwMiA4Ny43MTQ4IDkwLjMxMTUgODcuNTQzIDg5LjUzODEgODcuMTk5MkM4OC43NzE4IDg2Ljg1NTUgODguMTA1OCA4Ni4zNTc3IDg3LjU0IDg1LjcwNjFDODYuOTgxNCA4NS4wNTQ0IDg2LjU0ODIgODQuMjYzIDg2LjI0MDIgODMuMzMyQzg1LjkzMjMgODIuNDAxIDg1Ljc3ODMgODEuMzQ4MyA4NS43NzgzIDgwLjE3MzhWNzkuMTg1NUM4NS43NzgzIDc4LjAxMTEgODUuOTI4NyA3Ni45NjE5IDg2LjIyOTUgNzYuMDM4MUM4Ni41Mzc0IDc1LjEwNzEgODYuOTcwNyA3NC4zMTU4IDg3LjUyOTMgNzMuNjY0MUM4OC4wODc5IDczLjAwNTIgODguNzUwMyA3Mi41MDM5IDg5LjUxNjYgNzIuMTYwMkM5MC4yOSA3MS44MTY0IDkxLjEzODcgNzEuNjQ0NSA5Mi4wNjI1IDcxLjY0NDVDOTMuMDE1IDcxLjY0NDUgOTMuODc0MyA3MS44MTY0IDk0LjY0MDYgNzIuMTYwMkM5NS40MTQxIDcyLjUwMzkgOTYuMDc2NSA3My4wMDUyIDk2LjYyNzkgNzMuNjY0MUM5Ny4xODY1IDc0LjMxNTggOTcuNjEyNiA3NS4xMDcxIDk3LjkwNjIgNzYuMDM4MUM5OC4yMDcgNzYuOTYxOSA5OC4zNTc0IDc4LjAxMTEgOTguMzU3NCA3OS4xODU1Wk05Ni4zMDU3IDgwLjE3MzhWNzkuMTY0MUM5Ni4zMDU3IDc4LjIzMzEgOTYuMjA5IDc3LjQwOTUgOTYuMDE1NiA3Ni42OTM0Qzk1LjgyOTQgNzUuOTc3MiA5NS41NTM3IDc1LjM3NTcgOTUuMTg4NSA3NC44ODg3Qzk0LjgyMzIgNzQuNDAxNyA5NC4zNzU3IDc0LjAzMjkgOTMuODQ1NyA3My43ODIyQzkzLjMyMjkgNzMuNTMxNiA5Mi43Mjg1IDczLjQwNjIgOTIuMDYyNSA3My40MDYyQzkxLjQxOCA3My40MDYyIDkwLjgzNDMgNzMuNTMxNiA5MC4zMTE1IDczLjc4MjJDODkuNzk1OSA3NC4wMzI5IDg5LjM1MTkgNzQuNDAxNyA4OC45Nzk1IDc0Ljg4ODdDODguNjE0MyA3NS4zNzU3IDg4LjMzMTQgNzUuOTc3MiA4OC4xMzA5IDc2LjY5MzRDODcuOTMwMyA3Ny40MDk1IDg3LjgzMDEgNzguMjMzMSA4Ny44MzAxIDc5LjE2NDFWODAuMTczOEM4Ny44MzAxIDgxLjExMiA4Ny45MzAzIDgxLjk0MjcgODguMTMwOSA4Mi42NjZDODguMzMxNCA4My4zODIyIDg4LjYxNzggODMuOTg3MyA4OC45OTAyIDg0LjQ4MTRDODkuMzY5OCA4NC45Njg0IDg5LjgxNzQgODUuMzM3MiA5MC4zMzMgODUuNTg3OUM5MC44NTU4IDg1LjgzODUgOTEuNDM5NSA4NS45NjM5IDkyLjA4NCA4NS45NjM5QzkyLjc1NzIgODUuOTYzOSA5My4zNTUxIDg1LjgzODUgOTMuODc3OSA4NS41ODc5Qzk0LjQwMDcgODUuMzM3MiA5NC44NDExIDg0Ljk2ODQgOTUuMTk5MiA4NC40ODE0Qzk1LjU2NDUgODMuOTg3MyA5NS44NDAyIDgzLjM4MjIgOTYuMDI2NCA4Mi42NjZDOTYuMjEyNiA4MS45NDI3IDk2LjMwNTcgODEuMTEyIDk2LjMwNTcgODAuMTczOFpNMTEzLjQ5MyA3MS44NTk0Vjg3LjVIMTExLjQwOUwxMDMuNTM1IDc1LjQzNjVWODcuNUgxMDEuNDYyVjcxLjg1OTRIMTAzLjUzNUwxMTEuNDQxIDgzLjk1NTFWNzEuODU5NEgxMTMuNDkzWiIgZmlsbD0id2hpdGUiLz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfaV80NTg4Xzg4NDIwIiB4PSI0OC41IiB5PSIzNC41IiB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeD0iLTUiIGR5PSI1Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0iYXJpdGhtZXRpYyIgazI9Ii0xIiBrMz0iMSIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJzaGFwZSIgcmVzdWx0PSJlZmZlY3QxX2lubmVyU2hhZG93XzQ1ODhfODg0MjAiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTg4Xzg4NDIwIiB4PSIzNS41IiB5PSIyMC41IiB3aWR0aD0iMTI5IiBoZWlnaHQ9IjEyNyIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSI0Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA4IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDU4OF84ODQyMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTg4Xzg4NDIwIiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNDU4OF84ODQyMCIgeDE9IjY1Ljg5NjQiIHkxPSIxMjQuNSIgeDI9IjEyOS41MTkiIHkyPSIzMy45MjQ4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDQ0NDQ0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=", "description": "Sends the command to the device or updates attribute/time series when the user pushes the button. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when power on/off states.", "descriptor": { "type": "rpc", @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "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", + "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 overflowVisible: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-power-button-widget-settings", 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 new file mode 100644 index 0000000000..15c12e1b81 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/scada_symbol.json @@ -0,0 +1,24 @@ +{ + "fqn": "scada_symbol", + "name": "SCADA symbol", + "deprecated": false, + "scada": true, + "image": null, + "description": "", + "descriptor": { + "type": "rpc", + "sizeX": 3, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "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\":{\"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": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/toggle_button.json b/application/src/main/data/json/system/widget_types/toggle_button.json index f3f3449b13..e756b31e0f 100644 --- a/application/src/main/data/json/system/widget_types/toggle_button.json +++ b/application/src/main/data/json/system/widget_types/toggle_button.json @@ -2,7 +2,7 @@ "fqn": "toggle_button", "name": "Toggle button", "deprecated": false, - "image": "tb-image:VG9nZ2xlIGJ1dHRvbnMuc3Zn:IlRvZ2dsZSBidXR0b24iIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjE2IiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIxNiAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDY5N18zNjcxMykiPgo8cmVjdCB4PSI4Ljc1IiB5PSIxNi43NSIgd2lkdGg9IjE5OC41IiBoZWlnaHQ9IjU4LjUiIHJ4PSIzLjI1IiBzdHJva2U9IiMxOTgwMzgiIHN0cm9rZS13aWR0aD0iMS41IiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPG1hc2sgaWQ9Im1hc2swXzQ2OTdfMzY3MTMiIHN0eWxlPSJtYXNrLXR5cGU6YWxwaGEiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjYyIiB5PSIzNCIgd2lkdGg9IjI1IiBoZWlnaHQ9IjI0Ij4KPHJlY3QgeD0iNjIuNSIgeT0iMzQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNjguNSA0Mkg3Ny41VjQwQzc3LjUgMzkuMTY2NyA3Ny4yMDgzIDM4LjQ1ODMgNzYuNjI1IDM3Ljg3NUM3Ni4wNDE3IDM3LjI5MTcgNzUuMzMzMyAzNyA3NC41IDM3QzczLjY2NjcgMzcgNzIuOTU4MyAzNy4yOTE3IDcyLjM3NSAzNy44NzVDNzEuNzkxNyAzOC40NTgzIDcxLjUgMzkuMTY2NyA3MS41IDQwSDY5LjVDNjkuNSAzOC42MTY3IDY5Ljk4NzUgMzcuNDM3NSA3MC45NjI1IDM2LjQ2MjVDNzEuOTM3NSAzNS40ODc1IDczLjExNjcgMzUgNzQuNSAzNUM3NS44ODMzIDM1IDc3LjA2MjUgMzUuNDg3NSA3OC4wMzc1IDM2LjQ2MjVDNzkuMDEyNSAzNy40Mzc1IDc5LjUgMzguNjE2NyA3OS41IDQwVjQySDgwLjVDODEuMDUgNDIgODEuNTIwOCA0Mi4xOTU4IDgxLjkxMjUgNDIuNTg3NUM4Mi4zMDQyIDQyLjk3OTIgODIuNSA0My40NSA4Mi41IDQ0VjU0QzgyLjUgNTQuNTUgODIuMzA0MiA1NS4wMjA4IDgxLjkxMjUgNTUuNDEyNUM4MS41MjA4IDU1LjgwNDIgODEuMDUgNTYgODAuNSA1Nkg2OC41QzY3Ljk1IDU2IDY3LjQ3OTIgNTUuODA0MiA2Ny4wODc1IDU1LjQxMjVDNjYuNjk1OCA1NS4wMjA4IDY2LjUgNTQuNTUgNjYuNSA1NFY0NEM2Ni41IDQzLjQ1IDY2LjY5NTggNDIuOTc5MiA2Ny4wODc1IDQyLjU4NzVDNjcuNDc5MiA0Mi4xOTU4IDY3Ljk1IDQyIDY4LjUgNDJaTTY4LjUgNTRIODAuNVY0NEg2OC41VjU0Wk03NC41IDUxQzc1LjA1IDUxIDc1LjUyMDggNTAuODA0MiA3NS45MTI1IDUwLjQxMjVDNzYuMzA0MiA1MC4wMjA4IDc2LjUgNDkuNTUgNzYuNSA0OUM3Ni41IDQ4LjQ1IDc2LjMwNDIgNDcuOTc5MiA3NS45MTI1IDQ3LjU4NzVDNzUuNTIwOCA0Ny4xOTU4IDc1LjA1IDQ3IDc0LjUgNDdDNzMuOTUgNDcgNzMuNDc5MiA0Ny4xOTU4IDczLjA4NzUgNDcuNTg3NUM3Mi42OTU4IDQ3Ljk3OTIgNzIuNSA0OC40NSA3Mi41IDQ5QzcyLjUgNDkuNTUgNzIuNjk1OCA1MC4wMjA4IDczLjA4NzUgNTAuNDEyNUM3My40NzkyIDUwLjgwNDIgNzMuOTUgNTEgNzQuNSA1MVoiIGZpbGw9IiMxOTgwMzgiLz4KPC9nPgo8cGF0aCBkPSJNMTAyLjEzMSA0NS4yNVY0NS45NTMxQzEwMi4xMzEgNDYuOTE5OSAxMDIuMDA1IDQ3Ljc4NzEgMTAxLjc1MyA0OC41NTQ3QzEwMS41MDEgNDkuMzIyMyAxMDEuMTQxIDQ5Ljk3NTYgMTAwLjY3MiA1MC41MTQ2QzEwMC4yMDkgNTEuMDUzNyA5OS42NTIzIDUxLjQ2NjggOTkuMDAyIDUxLjc1MzlDOTguMzUxNiA1Mi4wMzUyIDk3LjYzMDkgNTIuMTc1OCA5Ni44Mzk4IDUyLjE3NThDOTYuMDU0NyA1Mi4xNzU4IDk1LjMzNjkgNTIuMDM1MiA5NC42ODY1IDUxLjc1MzlDOTQuMDQyIDUxLjQ2NjggOTMuNDgyNCA1MS4wNTM3IDkzLjAwNzggNTAuNTE0NkM5Mi41MzMyIDQ5Ljk3NTYgOTIuMTY0MSA0OS4zMjIzIDkxLjkwMDQgNDguNTU0N0M5MS42NDI2IDQ3Ljc4NzEgOTEuNTEzNyA0Ni45MTk5IDkxLjUxMzcgNDUuOTUzMVY0NS4yNUM5MS41MTM3IDQ0LjI4MzIgOTEuNjQyNiA0My40MTg5IDkxLjkwMDQgNDIuNjU3MkM5Mi4xNTgyIDQxLjg4OTYgOTIuNTIxNSA0MS4yMzYzIDkyLjk5MDIgNDAuNjk3M0M5My40NjQ4IDQwLjE1MjMgOTQuMDI0NCAzOS43MzkzIDk0LjY2ODkgMzkuNDU4Qzk1LjMxOTMgMzkuMTcwOSA5Ni4wMzcxIDM5LjAyNzMgOTYuODIyMyAzOS4wMjczQzk3LjYxMzMgMzkuMDI3MyA5OC4zMzQgMzkuMTcwOSA5OC45ODQ0IDM5LjQ1OEM5OS42MzQ4IDM5LjczOTMgMTAwLjE5NCA0MC4xNTIzIDEwMC42NjMgNDAuNjk3M0MxMDEuMTMyIDQxLjIzNjMgMTAxLjQ5MiA0MS44ODk2IDEwMS43NDQgNDIuNjU3MkMxMDIuMDAyIDQzLjQxODkgMTAyLjEzMSA0NC4yODMyIDEwMi4xMzEgNDUuMjVaTTk5LjkyNDggNDUuOTUzMVY0NS4yMzI0Qzk5LjkyNDggNDQuNTE3NiA5OS44NTQ1IDQzLjg4NzcgOTkuNzEzOSA0My4zNDI4Qzk5LjU3OTEgNDIuNzkyIDk5LjM3NyA0Mi4zMzIgOTkuMTA3NCA0MS45NjI5Qzk4Ljg0MzggNDEuNTg3OSA5OC41MTg2IDQxLjMwNjYgOTguMTMxOCA0MS4xMTkxQzk3Ljc0NTEgNDAuOTI1OCA5Ny4zMDg2IDQwLjgyOTEgOTYuODIyMyA0MC44MjkxQzk2LjMzNTkgNDAuODI5MSA5NS45MDIzIDQwLjkyNTggOTUuNTIxNSA0MS4xMTkxQzk1LjE0MDYgNDEuMzA2NiA5NC44MTU0IDQxLjU4NzkgOTQuNTQ1OSA0MS45NjI5Qzk0LjI4MjIgNDIuMzMyIDk0LjA4MDEgNDIuNzkyIDkzLjkzOTUgNDMuMzQyOEM5My43OTg4IDQzLjg4NzcgOTMuNzI4NSA0NC41MTc2IDkzLjcyODUgNDUuMjMyNFY0NS45NTMxQzkzLjcyODUgNDYuNjY4IDkzLjc5ODggNDcuMzAwOCA5My45Mzk1IDQ3Ljg1MTZDOTQuMDgwMSA0OC40MDIzIDk0LjI4NTIgNDguODY4MiA5NC41NTQ3IDQ5LjI0OUM5NC44MzAxIDQ5LjYyNCA5NS4xNTgyIDQ5LjkwODIgOTUuNTM5MSA1MC4xMDE2Qzk1LjkxOTkgNTAuMjg5MSA5Ni4zNTM1IDUwLjM4MjggOTYuODM5OCA1MC4zODI4Qzk3LjMzMiA1MC4zODI4IDk3Ljc2ODYgNTAuMjg5MSA5OC4xNDk0IDUwLjEwMTZDOTguNTMwMyA0OS45MDgyIDk4Ljg1MjUgNDkuNjI0IDk5LjExNjIgNDkuMjQ5Qzk5LjM3OTkgNDguODY4MiA5OS41NzkxIDQ4LjQwMjMgOTkuNzEzOSA0Ny44NTE2Qzk5Ljg1NDUgNDcuMzAwOCA5OS45MjQ4IDQ2LjY2OCA5OS45MjQ4IDQ1Ljk1MzFaTTEwNi40MDMgNDQuMzE4NFY1NS42NTYySDEwNC4yODVWNDIuNDkwMkgxMDYuMjM2TDEwNi40MDMgNDQuMzE4NFpNMTEyLjU5OSA0Ny4xNTcyVjQ3LjM0MThDMTEyLjU5OSA0OC4wMzMyIDExMi41MTcgNDguNjc0OCAxMTIuMzUzIDQ5LjI2NjZDMTEyLjE5NSA0OS44NTI1IDExMS45NTggNTAuMzY1MiAxMTEuNjQxIDUwLjgwNDdDMTExLjMzMSA1MS4yMzgzIDExMC45NDcgNTEuNTc1MiAxMTAuNDkgNTEuODE1NEMxMTAuMDMzIDUyLjA1NTcgMTA5LjUwNSA1Mi4xNzU4IDEwOC45MDggNTIuMTc1OEMxMDguMzE2IDUyLjE3NTggMTA3Ljc5NyA1Mi4wNjc0IDEwNy4zNTIgNTEuODUwNkMxMDYuOTEzIDUxLjYyNzkgMTA2LjU0MSA1MS4zMTQ1IDEwNi4yMzYgNTAuOTEwMkMxMDUuOTMxIDUwLjUwNTkgMTA1LjY4NSA1MC4wMzEyIDEwNS40OTggNDkuNDg2M0MxMDUuMzE2IDQ4LjkzNTUgMTA1LjE4NyA0OC4zMzIgMTA1LjExMSA0Ny42NzU4VjQ2Ljk2MzlDMTA1LjE4NyA0Ni4yNjY2IDEwNS4zMTYgNDUuNjMzOCAxMDUuNDk4IDQ1LjA2NTRDMTA1LjY4NSA0NC40OTcxIDEwNS45MzEgNDQuMDA3OCAxMDYuMjM2IDQzLjU5NzdDMTA2LjU0MSA0My4xODc1IDEwNi45MTMgNDIuODcxMSAxMDcuMzUyIDQyLjY0ODRDMTA3Ljc5MiA0Mi40MjU4IDEwOC4zMDQgNDIuMzE0NSAxMDguODkgNDIuMzE0NUMxMDkuNDg4IDQyLjMxNDUgMTEwLjAxOCA0Mi40MzE2IDExMC40ODEgNDIuNjY2QzExMC45NDQgNDIuODk0NSAxMTEuMzM0IDQzLjIyMjcgMTExLjY1IDQzLjY1MDRDMTExLjk2NiA0NC4wNzIzIDExMi4yMDQgNDQuNTgyIDExMi4zNjIgNDUuMTc5N0MxMTIuNTIgNDUuNzcxNSAxMTIuNTk5IDQ2LjQzMDcgMTEyLjU5OSA0Ny4xNTcyWk0xMTAuNDgxIDQ3LjM0MThWNDcuMTU3MkMxMTAuNDgxIDQ2LjcxNzggMTEwLjQ0IDQ2LjMxMDUgMTEwLjM1OCA0NS45MzU1QzExMC4yNzYgNDUuNTU0NyAxMTAuMTQ3IDQ1LjIyMDcgMTA5Ljk3MSA0NC45MzM2QzEwOS43OTYgNDQuNjQ2NSAxMDkuNTcgNDQuNDIzOCAxMDkuMjk1IDQ0LjI2NTZDMTA5LjAyNSA0NC4xMDE2IDEwOC43IDQ0LjAxOTUgMTA4LjMxOSA0NC4wMTk1QzEwNy45NDQgNDQuMDE5NSAxMDcuNjIyIDQ0LjA4NCAxMDcuMzUyIDQ0LjIxMjlDMTA3LjA4MyA0NC4zMzU5IDEwNi44NTcgNDQuNTA4OCAxMDYuNjc1IDQ0LjczMTRDMTA2LjQ5NCA0NC45NTQxIDEwNi4zNTMgNDUuMjE0OCAxMDYuMjU0IDQ1LjUxMzdDMTA2LjE1NCA0NS44MDY2IDEwNi4wODQgNDYuMTI2IDEwNi4wNDMgNDYuNDcxN1Y0OC4xNzY4QzEwNi4xMTMgNDguNTk4NiAxMDYuMjMzIDQ4Ljk4NTQgMTA2LjQwMyA0OS4zMzY5QzEwNi41NzMgNDkuNjg4NSAxMDYuODEzIDQ5Ljk2OTcgMTA3LjEyNCA1MC4xODA3QzEwNy40NCA1MC4zODU3IDEwNy44NDQgNTAuNDg4MyAxMDguMzM3IDUwLjQ4ODNDMTA4LjcxNyA1MC40ODgzIDEwOS4wNDMgNTAuNDA2MiAxMDkuMzEyIDUwLjI0MjJDMTA5LjU4MiA1MC4wNzgxIDEwOS44MDEgNDkuODUyNSAxMDkuOTcxIDQ5LjU2NTRDMTEwLjE0NyA0OS4yNzI1IDExMC4yNzYgNDguOTM1NSAxMTAuMzU4IDQ4LjU1NDdDMTEwLjQ0IDQ4LjE3MzggMTEwLjQ4MSA0Ny43Njk1IDExMC40ODEgNDcuMzQxOFpNMTE4Ljc0MyA1Mi4xNzU4QzExOC4wNCA1Mi4xNzU4IDExNy40MDQgNTIuMDYxNSAxMTYuODM2IDUxLjgzM0MxMTYuMjc0IDUxLjU5ODYgMTE1Ljc5MyA1MS4yNzM0IDExNS4zOTUgNTAuODU3NEMxMTUuMDAyIDUwLjQ0MTQgMTE0LjcgNDkuOTUyMSAxMTQuNDg5IDQ5LjM4OTZDMTE0LjI3OSA0OC44MjcxIDExNC4xNzMgNDguMjIwNyAxMTQuMTczIDQ3LjU3MDNWNDcuMjE4OEMxMTQuMTczIDQ2LjQ3NDYgMTE0LjI4MSA0NS44MDA4IDExNC40OTggNDUuMTk3M0MxMTQuNzE1IDQ0LjU5MzggMTE1LjAxNyA0NC4wNzgxIDExNS40MDQgNDMuNjUwNEMxMTUuNzkgNDMuMjE2OCAxMTYuMjQ3IDQyLjg4NTcgMTE2Ljc3NSA0Mi42NTcyQzExNy4zMDIgNDIuNDI4NyAxMTcuODczIDQyLjMxNDUgMTE4LjQ4OCA0Mi4zMTQ1QzExOS4xNjggNDIuMzE0NSAxMTkuNzYzIDQyLjQyODcgMTIwLjI3MyA0Mi42NTcyQzEyMC43ODIgNDIuODg1NyAxMjEuMjA0IDQzLjIwOCAxMjEuNTM4IDQzLjYyNEMxMjEuODc4IDQ0LjAzNDIgMTIyLjEzIDQ0LjUyMzQgMTIyLjI5NCA0NS4wOTE4QzEyMi40NjQgNDUuNjYwMiAxMjIuNTQ5IDQ2LjI4NzEgMTIyLjU0OSA0Ni45NzI3VjQ3Ljg3NzlIMTE1LjIwMVY0Ni4zNTc0SDEyMC40NTdWNDYuMTkwNEMxMjAuNDQ2IDQ1LjgwOTYgMTIwLjM2OSA0NS40NTIxIDEyMC4yMjkgNDUuMTE4MkMxMjAuMDk0IDQ0Ljc4NDIgMTE5Ljg4NiA0NC41MTQ2IDExOS42MDUgNDQuMzA5NkMxMTkuMzIzIDQ0LjEwNDUgMTE4Ljk0OCA0NC4wMDIgMTE4LjQ4IDQ0LjAwMkMxMTguMTI4IDQ0LjAwMiAxMTcuODE1IDQ0LjA3ODEgMTE3LjUzOSA0NC4yMzA1QzExNy4yNyA0NC4zNzcgMTE3LjA0NCA0NC41OTA4IDExNi44NjIgNDQuODcyMUMxMTYuNjgxIDQ1LjE1MzMgMTE2LjU0IDQ1LjQ5MzIgMTE2LjQ0MSA0NS44OTE2QzExNi4zNDcgNDYuMjg0MiAxMTYuMyA0Ni43MjY2IDExNi4zIDQ3LjIxODhWNDcuNTcwM0MxMTYuMyA0Ny45ODYzIDExNi4zNTYgNDguMzczIDExNi40NjcgNDguNzMwNUMxMTYuNTg0IDQ5LjA4MiAxMTYuNzU0IDQ5LjM4OTYgMTE2Ljk3NyA0OS42NTMzQzExNy4xOTkgNDkuOTE3IDExNy40NjkgNTAuMTI1IDExNy43ODUgNTAuMjc3M0MxMTguMTAyIDUwLjQyMzggMTE4LjQ2MiA1MC40OTcxIDExOC44NjYgNTAuNDk3MUMxMTkuMzc2IDUwLjQ5NzEgMTE5LjgzIDUwLjM5NDUgMTIwLjIyOSA1MC4xODk1QzEyMC42MjcgNDkuOTg0NCAxMjAuOTczIDQ5LjY5NDMgMTIxLjI2NiA0OS4zMTkzTDEyMi4zODIgNTAuNDAwNEMxMjIuMTc3IDUwLjY5OTIgMTIxLjkxIDUwLjk4NjMgMTIxLjU4MiA1MS4yNjE3QzEyMS4yNTQgNTEuNTMxMiAxMjAuODUzIDUxLjc1MSAxMjAuMzc4IDUxLjkyMDlDMTE5LjkwOSA1Mi4wOTA4IDExOS4zNjQgNTIuMTc1OCAxMTguNzQzIDUyLjE3NThaTTEyNi40NTIgNDQuNTIwNVY1MkgxMjQuMzM0VjQyLjQ5MDJIMTI2LjMyOUwxMjYuNDUyIDQ0LjUyMDVaTTEyNi4wNzQgNDYuODkzNkwxMjUuMzg4IDQ2Ljg4NDhDMTI1LjM5NCA0Ni4yMTA5IDEyNS40ODggNDUuNTkyOCAxMjUuNjcgNDUuMDMwM0MxMjUuODU3IDQ0LjQ2NzggMTI2LjExNSA0My45ODQ0IDEyNi40NDMgNDMuNTgwMUMxMjYuNzc3IDQzLjE3NTggMTI3LjE3NiA0Mi44NjUyIDEyNy42MzggNDIuNjQ4NEMxMjguMTAxIDQyLjQyNTggMTI4LjYxNyA0Mi4zMTQ1IDEyOS4xODUgNDIuMzE0NUMxMjkuNjQyIDQyLjMxNDUgMTMwLjA1NSA0Mi4zNzg5IDEzMC40MjUgNDIuNTA3OEMxMzAuOCA0Mi42MzA5IDEzMS4xMTkgNDIuODMzIDEzMS4zODMgNDMuMTE0M0MxMzEuNjUyIDQzLjM5NTUgMTMxLjg1NyA0My43NjE3IDEzMS45OTggNDQuMjEyOUMxMzIuMTM4IDQ0LjY1ODIgMTMyLjIwOSA0NS4yMDYxIDEzMi4yMDkgNDUuODU2NFY1MkgxMzAuMDgyVjQ1Ljg0NzdDMTMwLjA4MiA0NS4zOTA2IDEzMC4wMTQgNDUuMDMwMyAxMjkuODggNDQuNzY2NkMxMjkuNzUxIDQ0LjQ5NzEgMTI5LjU2IDQ0LjMwNjYgMTI5LjMwOCA0NC4xOTUzQzEyOS4wNjIgNDQuMDc4MSAxMjguNzU1IDQ0LjAxOTUgMTI4LjM4NiA0NC4wMTk1QzEyOC4wMjIgNDQuMDE5NSAxMjcuNjk3IDQ0LjA5NTcgMTI3LjQxIDQ0LjI0OEMxMjcuMTIzIDQ0LjQwMDQgMTI2Ljg4IDQ0LjYwODQgMTI2LjY4IDQ0Ljg3MjFDMTI2LjQ4NyA0NS4xMzU3IDEyNi4zMzggNDUuNDQwNCAxMjYuMjMyIDQ1Ljc4NjFDMTI2LjEyNyA0Ni4xMzE4IDEyNi4wNzQgNDYuNTAxIDEyNi4wNzQgNDYuODkzNlpNMTM4LjcxMyA1Mi4xNzU4QzEzOC4wMSA1Mi4xNzU4IDEzNy4zNzQgNTIuMDYxNSAxMzYuODA2IDUxLjgzM0MxMzYuMjQ0IDUxLjU5ODYgMTM1Ljc2MyA1MS4yNzM0IDEzNS4zNjUgNTAuODU3NEMxMzQuOTcyIDUwLjQ0MTQgMTM0LjY3IDQ5Ljk1MjEgMTM0LjQ1OSA0OS4zODk2QzEzNC4yNDggNDguODI3MSAxMzQuMTQzIDQ4LjIyMDcgMTM0LjE0MyA0Ny41NzAzVjQ3LjIxODhDMTM0LjE0MyA0Ni40NzQ2IDEzNC4yNTEgNDUuODAwOCAxMzQuNDY4IDQ1LjE5NzNDMTM0LjY4NSA0NC41OTM4IDEzNC45ODcgNDQuMDc4MSAxMzUuMzczIDQzLjY1MDRDMTM1Ljc2IDQzLjIxNjggMTM2LjIxNyA0Mi44ODU3IDEzNi43NDUgNDIuNjU3MkMxMzcuMjcyIDQyLjQyODcgMTM3Ljg0MyA0Mi4zMTQ1IDEzOC40NTggNDIuMzE0NUMxMzkuMTM4IDQyLjMxNDUgMTM5LjczMyA0Mi40Mjg3IDE0MC4yNDMgNDIuNjU3MkMxNDAuNzUyIDQyLjg4NTcgMTQxLjE3NCA0My4yMDggMTQxLjUwOCA0My42MjRDMTQxLjg0OCA0NC4wMzQyIDE0Mi4xIDQ0LjUyMzQgMTQyLjI2NCA0NS4wOTE4QzE0Mi40MzQgNDUuNjYwMiAxNDIuNTE5IDQ2LjI4NzEgMTQyLjUxOSA0Ni45NzI3VjQ3Ljg3NzlIMTM1LjE3MVY0Ni4zNTc0SDE0MC40MjdWNDYuMTkwNEMxNDAuNDE1IDQ1LjgwOTYgMTQwLjMzOSA0NS40NTIxIDE0MC4xOTkgNDUuMTE4MkMxNDAuMDY0IDQ0Ljc4NDIgMTM5Ljg1NiA0NC41MTQ2IDEzOS41NzUgNDQuMzA5NkMxMzkuMjkzIDQ0LjEwNDUgMTM4LjkxOCA0NC4wMDIgMTM4LjQ1IDQ0LjAwMkMxMzguMDk4IDQ0LjAwMiAxMzcuNzg1IDQ0LjA3ODEgMTM3LjUwOSA0NC4yMzA1QzEzNy4yNCA0NC4zNzcgMTM3LjAxNCA0NC41OTA4IDEzNi44MzIgNDQuODcyMUMxMzYuNjUxIDQ1LjE1MzMgMTM2LjUxIDQ1LjQ5MzIgMTM2LjQxMSA0NS44OTE2QzEzNi4zMTcgNDYuMjg0MiAxMzYuMjcgNDYuNzI2NiAxMzYuMjcgNDcuMjE4OFY0Ny41NzAzQzEzNi4yNyA0Ny45ODYzIDEzNi4zMjYgNDguMzczIDEzNi40MzcgNDguNzMwNUMxMzYuNTU0IDQ5LjA4MiAxMzYuNzI0IDQ5LjM4OTYgMTM2Ljk0NyA0OS42NTMzQzEzNy4xNjkgNDkuOTE3IDEzNy40MzkgNTAuMTI1IDEzNy43NTUgNTAuMjc3M0MxMzguMDcyIDUwLjQyMzggMTM4LjQzMiA1MC40OTcxIDEzOC44MzYgNTAuNDk3MUMxMzkuMzQ2IDUwLjQ5NzEgMTM5LjggNTAuMzk0NSAxNDAuMTk5IDUwLjE4OTVDMTQwLjU5NyA0OS45ODQ0IDE0MC45NDMgNDkuNjk0MyAxNDEuMjM2IDQ5LjMxOTNMMTQyLjM1MiA1MC40MDA0QzE0Mi4xNDcgNTAuNjk5MiAxNDEuODggNTAuOTg2MyAxNDEuNTUyIDUxLjI2MTdDMTQxLjIyNCA1MS41MzEyIDE0MC44MjMgNTEuNzUxIDE0MC4zNDggNTEuOTIwOUMxMzkuODc5IDUyLjA5MDggMTM5LjMzNCA1Mi4xNzU4IDEzOC43MTMgNTIuMTc1OFpNMTUwLjEyMiA1MC4wMzEyVjM4LjVIMTUyLjI0OVY1MkgxNTAuMzI0TDE1MC4xMjIgNTAuMDMxMlpNMTQzLjkzNSA0Ny4zNTA2VjQ3LjE2NkMxNDMuOTM1IDQ2LjQ0NTMgMTQ0LjAyIDQ1Ljc4OTEgMTQ0LjE4OSA0NS4xOTczQzE0NC4zNTkgNDQuNTk5NiAxNDQuNjA1IDQ0LjA4NjkgMTQ0LjkyOCA0My42NTkyQzE0NS4yNSA0My4yMjU2IDE0NS42NDMgNDIuODk0NSAxNDYuMTA1IDQyLjY2NkMxNDYuNTY4IDQyLjQzMTYgMTQ3LjA5IDQyLjMxNDUgMTQ3LjY3IDQyLjMxNDVDMTQ4LjI0NCA0Mi4zMTQ1IDE0OC43NDggNDIuNDI1OCAxNDkuMTgyIDQyLjY0ODRDMTQ5LjYxNSA0Mi44NzExIDE0OS45ODQgNDMuMTkwNCAxNTAuMjg5IDQzLjYwNjRDMTUwLjU5NCA0NC4wMTY2IDE1MC44MzcgNDQuNTA4OCAxNTEuMDE5IDQ1LjA4M0MxNTEuMiA0NS42NTE0IDE1MS4zMjkgNDYuMjg0MiAxNTEuNDA1IDQ2Ljk4MTRWNDcuNTcwM0MxNTEuMzI5IDQ4LjI1IDE1MS4yIDQ4Ljg3MTEgMTUxLjAxOSA0OS40MzM2QzE1MC44MzcgNDkuOTk2MSAxNTAuNTk0IDUwLjQ4MjQgMTUwLjI4OSA1MC44OTI2QzE0OS45ODQgNTEuMzAyNyAxNDkuNjEyIDUxLjYxOTEgMTQ5LjE3MyA1MS44NDE4QzE0OC43MzkgNTIuMDY0NSAxNDguMjMyIDUyLjE3NTggMTQ3LjY1MiA1Mi4xNzU4QzE0Ny4wNzggNTIuMTc1OCAxNDYuNTYgNTIuMDU1NyAxNDYuMDk3IDUxLjgxNTRDMTQ1LjY0IDUxLjU3NTIgMTQ1LjI1IDUxLjIzODMgMTQ0LjkyOCA1MC44MDQ3QzE0NC42MDUgNTAuMzcxMSAxNDQuMzU5IDQ5Ljg2MTMgMTQ0LjE4OSA0OS4yNzU0QzE0NC4wMiA0OC42ODM2IDE0My45MzUgNDguMDQyIDE0My45MzUgNDcuMzUwNlpNMTQ2LjA1MyA0Ny4xNjZWNDcuMzUwNkMxNDYuMDUzIDQ3Ljc4NDIgMTQ2LjA5MSA0OC4xODg1IDE0Ni4xNjcgNDguNTYzNUMxNDYuMjQ5IDQ4LjkzODUgMTQ2LjM3NSA0OS4yNjk1IDE0Ni41NDUgNDkuNTU2NkMxNDYuNzE1IDQ5LjgzNzkgMTQ2LjkzNSA1MC4wNjA1IDE0Ny4yMDQgNTAuMjI0NkMxNDcuNDc5IDUwLjM4MjggMTQ3LjgwOCA1MC40NjE5IDE0OC4xODggNTAuNDYxOUMxNDguNjY5IDUwLjQ2MTkgMTQ5LjA2NCA1MC4zNTY0IDE0OS4zNzUgNTAuMTQ1NUMxNDkuNjg2IDQ5LjkzNDYgMTQ5LjkyOSA0OS42NTA0IDE1MC4xMDQgNDkuMjkzQzE1MC4yODYgNDguOTI5NyAxNTAuNDA5IDQ4LjUyNTQgMTUwLjQ3NCA0OC4wODAxVjQ2LjQ4OTNDMTUwLjQzOCA0Ni4xNDM2IDE1MC4zNjUgNDUuODIxMyAxNTAuMjU0IDQ1LjUyMjVDMTUwLjE0OCA0NS4yMjM2IDE1MC4wMDUgNDQuOTYyOSAxNDkuODIzIDQ0Ljc0MDJDMTQ5LjY0MiA0NC41MTE3IDE0OS40MTYgNDQuMzM1OSAxNDkuMTQ2IDQ0LjIxMjlDMTQ4Ljg4MyA0NC4wODQgMTQ4LjU2OSA0NC4wMTk1IDE0OC4yMDYgNDQuMDE5NUMxNDcuODE5IDQ0LjAxOTUgMTQ3LjQ5MSA0NC4xMDE2IDE0Ny4yMjIgNDQuMjY1NkMxNDYuOTUyIDQ0LjQyOTcgMTQ2LjcyOSA0NC42NTUzIDE0Ni41NTQgNDQuOTQyNEMxNDYuMzg0IDQ1LjIyOTUgMTQ2LjI1OCA0NS41NjM1IDE0Ni4xNzYgNDUuOTQ0M0MxNDYuMDk0IDQ2LjMyNTIgMTQ2LjA1MyA0Ni43MzI0IDE0Ni4wNTMgNDcuMTY2WiIgZmlsbD0iIzE5ODAzOCIvPgo8L2c+CjxtYXNrIGlkPSJwYXRoLTYtaW5zaWRlLTFfNDY5N18zNjcxMyIgZmlsbD0id2hpdGUiPgo8cGF0aCBkPSJNOCA4OEM4IDg1Ljc5MDkgOS43OTA4NiA4NCAxMiA4NEgyMDRDMjA2LjIwOSA4NCAyMDggODUuNzkwOSAyMDggODhWMTQwQzIwOCAxNDIuMjA5IDIwNi4yMDkgMTQ0IDIwNCAxNDRIMTJDOS43OTA4NiAxNDQgOCAxNDIuMjA5IDggMTQwVjg4WiIvPgo8L21hc2s+CjxwYXRoIGQ9Ik04IDg4QzggODUuNzkwOSA5Ljc5MDg2IDg0IDEyIDg0SDIwNEMyMDYuMjA5IDg0IDIwOCA4NS43OTA5IDIwOCA4OFYxNDBDMjA4IDE0Mi4yMDkgMjA2LjIwOSAxNDQgMjA0IDE0NEgxMkM5Ljc5MDg2IDE0NCA4IDE0Mi4yMDkgOCAxNDBWODhaIiBmaWxsPSIjRDEyNzMwIi8+CjxwYXRoIGQ9Ik04IDg0SDIwOEg4Wk0yMDggMTQwQzIwOCAxNDMuMzE0IDIwNS4zMTQgMTQ2IDIwMiAxNDZIMTRDMTAuNjg2MyAxNDYgOCAxNDMuMzE0IDggMTQwQzggMTQxLjEwNSA5Ljc5MDg2IDE0MiAxMiAxNDJIMjA0QzIwNi4yMDkgMTQyIDIwOCAxNDEuMTA1IDIwOCAxNDBaTTggMTQ0Vjg0VjE0NFpNMjA4IDg0VjE0NFY4NFoiIGZpbGw9IiNEMTI3MzAiIG1hc2s9InVybCgjcGF0aC02LWluc2lkZS0xXzQ2OTdfMzY3MTMpIi8+CjxtYXNrIGlkPSJtYXNrMV80Njk3XzM2NzEzIiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSI2NSIgeT0iMTAyIiB3aWR0aD0iMjUiIGhlaWdodD0iMjQiPgo8cmVjdCB4PSI2NS41IiB5PSIxMDIiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazFfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNzEuNSAxMjRDNzAuOTUgMTI0IDcwLjQ3OTIgMTIzLjgwNCA3MC4wODc1IDEyMy40MTNDNjkuNjk1OCAxMjMuMDIxIDY5LjUgMTIyLjU1IDY5LjUgMTIyVjExMkM2OS41IDExMS40NSA2OS42OTU4IDExMC45NzkgNzAuMDg3NSAxMTAuNTg4QzcwLjQ3OTIgMTEwLjE5NiA3MC45NSAxMTAgNzEuNSAxMTBINzIuNVYxMDhDNzIuNSAxMDYuNjE3IDcyLjk4NzUgMTA1LjQzOCA3My45NjI1IDEwNC40NjJDNzQuOTM3NSAxMDMuNDg3IDc2LjExNjcgMTAzIDc3LjUgMTAzQzc4Ljg4MzMgMTAzIDgwLjA2MjUgMTAzLjQ4NyA4MS4wMzc1IDEwNC40NjJDODIuMDEyNSAxMDUuNDM4IDgyLjUgMTA2LjYxNyA4Mi41IDEwOFYxMTBIODMuNUM4NC4wNSAxMTAgODQuNTIwOCAxMTAuMTk2IDg0LjkxMjUgMTEwLjU4OEM4NS4zMDQyIDExMC45NzkgODUuNSAxMTEuNDUgODUuNSAxMTJWMTIyQzg1LjUgMTIyLjU1IDg1LjMwNDIgMTIzLjAyMSA4NC45MTI1IDEyMy40MTNDODQuNTIwOCAxMjMuODA0IDg0LjA1IDEyNCA4My41IDEyNEg3MS41Wk03MS41IDEyMkg4My41VjExMkg3MS41VjEyMlpNNzcuNSAxMTlDNzguMDUgMTE5IDc4LjUyMDggMTE4LjgwNCA3OC45MTI1IDExOC40MTNDNzkuMzA0MiAxMTguMDIxIDc5LjUgMTE3LjU1IDc5LjUgMTE3Qzc5LjUgMTE2LjQ1IDc5LjMwNDIgMTE1Ljk3OSA3OC45MTI1IDExNS41ODhDNzguNTIwOCAxMTUuMTk2IDc4LjA1IDExNSA3Ny41IDExNUM3Ni45NSAxMTUgNzYuNDc5MiAxMTUuMTk2IDc2LjA4NzUgMTE1LjU4OEM3NS42OTU4IDExNS45NzkgNzUuNSAxMTYuNDUgNzUuNSAxMTdDNzUuNSAxMTcuNTUgNzUuNjk1OCAxMTguMDIxIDc2LjA4NzUgMTE4LjQxM0M3Ni40NzkyIDExOC44MDQgNzYuOTUgMTE5IDc3LjUgMTE5Wk03NC41IDExMEg4MC41VjEwOEM4MC41IDEwNy4xNjcgODAuMjA4MyAxMDYuNDU4IDc5LjYyNSAxMDUuODc1Qzc5LjA0MTcgMTA1LjI5MiA3OC4zMzMzIDEwNSA3Ny41IDEwNUM3Ni42NjY3IDEwNSA3NS45NTgzIDEwNS4yOTIgNzUuMzc1IDEwNS44NzVDNzQuNzkxNyAxMDYuNDU4IDc0LjUgMTA3LjE2NyA3NC41IDEwOFYxMTBaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxwYXRoIGQ9Ik0xMDIuNjc2IDExNS44MzRIMTA0Ljg3M0MxMDQuODAzIDExNi42NzIgMTA0LjU2OCAxMTcuNDE5IDEwNC4xNyAxMTguMDc1QzEwMy43NzEgMTE4LjcyNiAxMDMuMjEyIDExOS4yMzggMTAyLjQ5MSAxMTkuNjEzQzEwMS43NzEgMTE5Ljk4OCAxMDAuODk1IDEyMC4xNzYgOTkuODYzMyAxMjAuMTc2Qzk5LjA3MjMgMTIwLjE3NiA5OC4zNjA0IDEyMC4wMzUgOTcuNzI3NSAxMTkuNzU0Qzk3LjA5NDcgMTE5LjQ2NyA5Ni41NTI3IDExOS4wNjIgOTYuMTAxNiAxMTguNTQxQzk1LjY1MDQgMTE4LjAxNCA5NS4zMDQ3IDExNy4zNzggOTUuMDY0NSAxMTYuNjM0Qzk0LjgzMDEgMTE1Ljg5IDk0LjcxMjkgMTE1LjA1OCA5NC43MTI5IDExNC4xMzhWMTEzLjA3NEM5NC43MTI5IDExMi4xNTQgOTQuODMzIDExMS4zMjIgOTUuMDczMiAxMTAuNTc4Qzk1LjMxOTMgMTA5LjgzNCA5NS42NzA5IDEwOS4xOTggOTYuMTI3OSAxMDguNjcxQzk2LjU4NSAxMDguMTM4IDk3LjEzMjggMTA3LjczIDk3Ljc3MTUgMTA3LjQ0OUM5OC40MTYgMTA3LjE2OCA5OS4xMzk2IDEwNy4wMjcgOTkuOTQyNCAxMDcuMDI3QzEwMC45NjIgMTA3LjAyNyAxMDEuODIzIDEwNy4yMTUgMTAyLjUyNiAxMDcuNTlDMTAzLjIyOSAxMDcuOTY1IDEwMy43NzQgMTA4LjQ4MyAxMDQuMTYxIDEwOS4xNDZDMTA0LjU1NCAxMDkuODA4IDEwNC43OTQgMTEwLjU2NiAxMDQuODgyIDExMS40MjJIMTAyLjY4NUMxMDIuNjI2IDExMC44NzEgMTAyLjQ5NyAxMTAuMzk5IDEwMi4yOTggMTEwLjAwN0MxMDIuMTA0IDEwOS42MTQgMTAxLjgxNyAxMDkuMzE1IDEwMS40MzcgMTA5LjExQzEwMS4wNTYgMTA4Ljg5OSAxMDAuNTU4IDEwOC43OTQgOTkuOTQyNCAxMDguNzk0Qzk5LjQzODUgMTA4Ljc5NCA5OC45OTkgMTA4Ljg4OCA5OC42MjQgMTA5LjA3NUM5OC4yNDkgMTA5LjI2MyA5Ny45MzU1IDEwOS41MzggOTcuNjgzNiAxMDkuOTAxQzk3LjQzMTYgMTEwLjI2NSA5Ny4yNDEyIDExMC43MTMgOTcuMTEyMyAxMTEuMjQ2Qzk2Ljk4OTMgMTExLjc3MyA5Ni45Mjc3IDExMi4zNzcgOTYuOTI3NyAxMTMuMDU3VjExNC4xMzhDOTYuOTI3NyAxMTQuNzgyIDk2Ljk4MzQgMTE1LjM2OCA5Ny4wOTQ3IDExNS44OTZDOTcuMjExOSAxMTYuNDE3IDk3LjM4NzcgMTE2Ljg2NSA5Ny42MjIxIDExNy4yNEM5Ny44NjIzIDExNy42MTUgOTguMTY3IDExNy45MDUgOTguNTM2MSAxMTguMTFDOTguOTA1MyAxMTguMzE1IDk5LjM0NzcgMTE4LjQxOCA5OS44NjMzIDExOC40MThDMTAwLjQ5IDExOC40MTggMTAwLjk5NyAxMTguMzE4IDEwMS4zODQgMTE4LjExOUMxMDEuNzc2IDExNy45MiAxMDIuMDcyIDExNy42MyAxMDIuMjcxIDExNy4yNDlDMTAyLjQ3NyAxMTYuODYyIDEwMi42MTEgMTE2LjM5MSAxMDIuNjc2IDExNS44MzRaTTEwOS4wODQgMTA2LjVWMTIwSDEwNi45NTdWMTA2LjVIMTA5LjA4NFpNMTExLjE1IDExNS4zNTFWMTE1LjE0OEMxMTEuMTUgMTE0LjQ2MyAxMTEuMjQ5IDExMy44MjcgMTExLjQ0OCAxMTMuMjQxQzExMS42NDggMTEyLjY0OSAxMTEuOTM1IDExMi4xMzcgMTEyLjMxIDExMS43MDNDMTEyLjY5MSAxMTEuMjY0IDExMy4xNTQgMTEwLjkyNCAxMTMuNjk4IDExMC42ODRDMTE0LjI0OSAxMTAuNDM4IDExNC44NyAxMTAuMzE0IDExNS41NjIgMTEwLjMxNEMxMTYuMjU5IDExMC4zMTQgMTE2Ljg4IDExMC40MzggMTE3LjQyNSAxMTAuNjg0QzExNy45NzYgMTEwLjkyNCAxMTguNDQyIDExMS4yNjQgMTE4LjgyMiAxMTEuNzAzQzExOS4yMDMgMTEyLjEzNyAxMTkuNDkzIDExMi42NDkgMTE5LjY5MyAxMTMuMjQxQzExOS44OTIgMTEzLjgyNyAxMTkuOTkxIDExNC40NjMgMTE5Ljk5MSAxMTUuMTQ4VjExNS4zNTFDMTE5Ljk5MSAxMTYuMDM2IDExOS44OTIgMTE2LjY3MiAxMTkuNjkzIDExNy4yNThDMTE5LjQ5MyAxMTcuODQ0IDExOS4yMDMgMTE4LjM1NiAxMTguODIyIDExOC43OTZDMTE4LjQ0MiAxMTkuMjI5IDExNy45NzkgMTE5LjU2OSAxMTcuNDM0IDExOS44MTVDMTE2Ljg4OSAxMjAuMDU2IDExNi4yNzEgMTIwLjE3NiAxMTUuNTc5IDEyMC4xNzZDMTE0Ljg4MiAxMjAuMTc2IDExNC4yNTggMTIwLjA1NiAxMTMuNzA3IDExOS44MTVDMTEzLjE2MiAxMTkuNTY5IDExMi42OTkgMTE5LjIyOSAxMTIuMzE5IDExOC43OTZDMTExLjkzOCAxMTguMzU2IDExMS42NDggMTE3Ljg0NCAxMTEuNDQ4IDExNy4yNThDMTExLjI0OSAxMTYuNjcyIDExMS4xNSAxMTYuMDM2IDExMS4xNSAxMTUuMzUxWk0xMTMuMjY4IDExNS4xNDhWMTE1LjM1MUMxMTMuMjY4IDExNS43NzggMTEzLjMxMiAxMTYuMTgzIDExMy40IDExNi41NjNDMTEzLjQ4NyAxMTYuOTQ0IDExMy42MjUgMTE3LjI3OCAxMTMuODEzIDExNy41NjVDMTE0IDExNy44NTMgMTE0LjI0IDExOC4wNzggMTE0LjUzMyAxMTguMjQyQzExNC44MjYgMTE4LjQwNiAxMTUuMTc1IDExOC40ODggMTE1LjU3OSAxMTguNDg4QzExNS45NzIgMTE4LjQ4OCAxMTYuMzEyIDExOC40MDYgMTE2LjU5OSAxMTguMjQyQzExNi44OTIgMTE4LjA3OCAxMTcuMTMyIDExNy44NTMgMTE3LjMyIDExNy41NjVDMTE3LjUwNyAxMTcuMjc4IDExNy42NDUgMTE2Ljk0NCAxMTcuNzMzIDExNi41NjNDMTE3LjgyNiAxMTYuMTgzIDExNy44NzMgMTE1Ljc3OCAxMTcuODczIDExNS4zNTFWMTE1LjE0OEMxMTcuODczIDExNC43MjcgMTE3LjgyNiAxMTQuMzI4IDExNy43MzMgMTEzLjk1M0MxMTcuNjQ1IDExMy41NzIgMTE3LjUwNCAxMTMuMjM1IDExNy4zMTEgMTEyLjk0MkMxMTcuMTIzIDExMi42NDkgMTE2Ljg4MyAxMTIuNDIxIDExNi41OSAxMTIuMjU3QzExNi4zMDMgMTEyLjA4NyAxMTUuOTYgMTEyLjAwMiAxMTUuNTYyIDExMi4wMDJDMTE1LjE2MyAxMTIuMDAyIDExNC44MTggMTEyLjA4NyAxMTQuNTI1IDExMi4yNTdDMTE0LjIzNyAxMTIuNDIxIDExNCAxMTIuNjQ5IDExMy44MTMgMTEyLjk0MkMxMTMuNjI1IDExMy4yMzUgMTEzLjQ4NyAxMTMuNTcyIDExMy40IDExMy45NTNDMTEzLjMxMiAxMTQuMzI4IDExMy4yNjggMTE0LjcyNyAxMTMuMjY4IDExNS4xNDhaTTEyNy4yNTIgMTE3LjQyNUMxMjcuMjUyIDExNy4yMTQgMTI3LjE5OSAxMTcuMDIzIDEyNy4wOTQgMTE2Ljg1NEMxMjYuOTg4IDExNi42NzggMTI2Ljc4NiAxMTYuNTIgMTI2LjQ4NyAxMTYuMzc5QzEyNi4xOTQgMTE2LjIzOCAxMjUuNzYxIDExNi4xMDkgMTI1LjE4NiAxMTUuOTkyQzEyNC42ODIgMTE1Ljg4MSAxMjQuMjIgMTE1Ljc0OSAxMjMuNzk4IDExNS41OTdDMTIzLjM4MiAxMTUuNDM4IDEyMy4wMjQgMTE1LjI0OCAxMjIuNzI1IDExNS4wMjVDMTIyLjQyNyAxMTQuODAzIDEyMi4xOTUgMTE0LjUzOSAxMjIuMDMxIDExNC4yMzRDMTIxLjg2NyAxMTMuOTMgMTIxLjc4NSAxMTMuNTc4IDEyMS43ODUgMTEzLjE4QzEyMS43ODUgMTEyLjc5MyAxMjEuODcgMTEyLjQyNyAxMjIuMDQgMTEyLjA4MUMxMjIuMjEgMTExLjczNSAxMjIuNDUzIDExMS40MzEgMTIyLjc2OSAxMTEuMTY3QzEyMy4wODYgMTEwLjkwMyAxMjMuNDcgMTEwLjY5NSAxMjMuOTIxIDExMC41NDNDMTI0LjM3OCAxMTAuMzkxIDEyNC44ODggMTEwLjMxNCAxMjUuNDUgMTEwLjMxNEMxMjYuMjQ3IDExMC4zMTQgMTI2LjkyOSAxMTAuNDQ5IDEyNy40OTggMTEwLjcxOUMxMjguMDcyIDExMC45ODIgMTI4LjUxMiAxMTEuMzQzIDEyOC44MTYgMTExLjhDMTI5LjEyMSAxMTIuMjUxIDEyOS4yNzMgMTEyLjc2MSAxMjkuMjczIDExMy4zMjlIMTI3LjE1NUMxMjcuMTU1IDExMy4wNzcgMTI3LjA5MSAxMTIuODQzIDEyNi45NjIgMTEyLjYyNkMxMjYuODM5IDExMi40MDMgMTI2LjY1MSAxMTIuMjI1IDEyNi4zOTkgMTEyLjA5QzEyNi4xNDcgMTExLjk0OSAxMjUuODMxIDExMS44NzkgMTI1LjQ1IDExMS44NzlDMTI1LjA4NyAxMTEuODc5IDEyNC43ODUgMTExLjkzOCAxMjQuNTQ1IDExMi4wNTVDMTI0LjMxIDExMi4xNjYgMTI0LjEzNSAxMTIuMzEyIDEyNC4wMTcgMTEyLjQ5NEMxMjMuOTA2IDExMi42NzYgMTIzLjg1IDExMi44NzUgMTIzLjg1IDExMy4wOTJDMTIzLjg1IDExMy4yNSAxMjMuODggMTEzLjM5NCAxMjMuOTM4IDExMy41MjJDMTI0LjAwMyAxMTMuNjQ2IDEyNC4xMDggMTEzLjc2IDEyNC4yNTUgMTEzLjg2NUMxMjQuNDAxIDExMy45NjUgMTI0LjYgMTE0LjA1OSAxMjQuODUyIDExNC4xNDZDMTI1LjExIDExNC4yMzQgMTI1LjQzMiAxMTQuMzE5IDEyNS44MTkgMTE0LjQwMUMxMjYuNTQ2IDExNC41NTQgMTI3LjE3IDExNC43NSAxMjcuNjkxIDExNC45OUMxMjguMjE5IDExNS4yMjUgMTI4LjYyMyAxMTUuNTI5IDEyOC45MDQgMTE1LjkwNEMxMjkuMTg1IDExNi4yNzMgMTI5LjMyNiAxMTYuNzQyIDEyOS4zMjYgMTE3LjMxMUMxMjkuMzI2IDExNy43MzIgMTI5LjIzNSAxMTguMTE5IDEyOS4wNTQgMTE4LjQ3MUMxMjguODc4IDExOC44MTYgMTI4LjYyIDExOS4xMTggMTI4LjI4IDExOS4zNzZDMTI3Ljk0IDExOS42MjggMTI3LjUzMyAxMTkuODI0IDEyNy4wNTggMTE5Ljk2NUMxMjYuNTkgMTIwLjEwNSAxMjYuMDYyIDEyMC4xNzYgMTI1LjQ3NiAxMjAuMTc2QzEyNC42MTUgMTIwLjE3NiAxMjMuODg2IDEyMC4wMjMgMTIzLjI4OCAxMTkuNzE5QzEyMi42OSAxMTkuNDA4IDEyMi4yMzYgMTE5LjAxMyAxMjEuOTI2IDExOC41MzJDMTIxLjYyMSAxMTguMDQ2IDEyMS40NjkgMTE3LjU0MiAxMjEuNDY5IDExNy4wMjFIMTIzLjUxNkMxMjMuNTQgMTE3LjQxMyAxMjMuNjQ4IDExNy43MjcgMTIzLjg0MiAxMTcuOTYxQzEyNC4wNDEgMTE4LjE4OSAxMjQuMjg3IDExOC4zNTYgMTI0LjU4IDExOC40NjJDMTI0Ljg3OSAxMTguNTYyIDEyNS4xODYgMTE4LjYxMSAxMjUuNTAzIDExOC42MTFDMTI1Ljg4NCAxMTguNjExIDEyNi4yMDMgMTE4LjU2MiAxMjYuNDYxIDExOC40NjJDMTI2LjcxOSAxMTguMzU2IDEyNi45MTUgMTE4LjIxNiAxMjcuMDUgMTE4LjA0QzEyNy4xODQgMTE3Ljg1OCAxMjcuMjUyIDExNy42NTMgMTI3LjI1MiAxMTcuNDI1Wk0xMzUuNTIzIDEyMC4xNzZDMTM0LjgyIDEyMC4xNzYgMTM0LjE4NCAxMjAuMDYyIDEzMy42MTYgMTE5LjgzM0MxMzMuMDUzIDExOS41OTkgMTMyLjU3MyAxMTkuMjczIDEzMi4xNzQgMTE4Ljg1N0MxMzEuNzgyIDExOC40NDEgMTMxLjQ4IDExNy45NTIgMTMxLjI2OSAxMTcuMzlDMTMxLjA1OCAxMTYuODI3IDEzMC45NTMgMTE2LjIyMSAxMzAuOTUzIDExNS41N1YxMTUuMjE5QzEzMC45NTMgMTE0LjQ3NSAxMzEuMDYxIDExMy44MDEgMTMxLjI3OCAxMTMuMTk3QzEzMS40OTUgMTEyLjU5NCAxMzEuNzk2IDExMi4wNzggMTMyLjE4MyAxMTEuNjVDMTMyLjU3IDExMS4yMTcgMTMzLjAyNyAxMTAuODg2IDEzMy41NTQgMTEwLjY1N0MxMzQuMDgxIDExMC40MjkgMTM0LjY1MyAxMTAuMzE0IDEzNS4yNjggMTEwLjMxNEMxMzUuOTQ4IDExMC4zMTQgMTM2LjU0MiAxMTAuNDI5IDEzNy4wNTIgMTEwLjY1N0MxMzcuNTYyIDExMC44ODYgMTM3Ljk4NCAxMTEuMjA4IDEzOC4zMTggMTExLjYyNEMxMzguNjU4IDExMi4wMzQgMTM4LjkxIDExMi41MjMgMTM5LjA3NCAxMTMuMDkyQzEzOS4yNDQgMTEzLjY2IDEzOS4zMjkgMTE0LjI4NyAxMzkuMzI5IDExNC45NzNWMTE1Ljg3OEgxMzEuOTgxVjExNC4zNTdIMTM3LjIzN1YxMTQuMTlDMTM3LjIyNSAxMTMuODEgMTM3LjE0OSAxMTMuNDUyIDEzNy4wMDggMTEzLjExOEMxMzYuODczIDExMi43ODQgMTM2LjY2NSAxMTIuNTE1IDEzNi4zODQgMTEyLjMxQzEzNi4xMDMgMTEyLjEwNCAxMzUuNzI4IDExMi4wMDIgMTM1LjI1OSAxMTIuMDAyQzEzNC45MDggMTEyLjAwMiAxMzQuNTk0IDExMi4wNzggMTM0LjMxOSAxMTIuMjNDMTM0LjA0OSAxMTIuMzc3IDEzMy44MjQgMTEyLjU5MSAxMzMuNjQyIDExMi44NzJDMTMzLjQ2IDExMy4xNTMgMTMzLjMyIDExMy40OTMgMTMzLjIyIDExMy44OTJDMTMzLjEyNiAxMTQuMjg0IDEzMy4wNzkgMTE0LjcyNyAxMzMuMDc5IDExNS4yMTlWMTE1LjU3QzEzMy4wNzkgMTE1Ljk4NiAxMzMuMTM1IDExNi4zNzMgMTMzLjI0NiAxMTYuNzNDMTMzLjM2NCAxMTcuMDgyIDEzMy41MzQgMTE3LjM5IDEzMy43NTYgMTE3LjY1M0MxMzMuOTc5IDExNy45MTcgMTM0LjI0OCAxMTguMTI1IDEzNC41NjUgMTE4LjI3N0MxMzQuODgxIDExOC40MjQgMTM1LjI0MiAxMTguNDk3IDEzNS42NDYgMTE4LjQ5N0MxMzYuMTU2IDExOC40OTcgMTM2LjYxIDExOC4zOTUgMTM3LjAwOCAxMTguMTg5QzEzNy40MDcgMTE3Ljk4NCAxMzcuNzUyIDExNy42OTQgMTM4LjA0NSAxMTcuMzE5TDEzOS4xNjIgMTE4LjRDMTM4Ljk1NiAxMTguNjk5IDEzOC42OSAxMTguOTg2IDEzOC4zNjIgMTE5LjI2MkMxMzguMDM0IDExOS41MzEgMTM3LjYzMiAxMTkuNzUxIDEzNy4xNTggMTE5LjkyMUMxMzYuNjg5IDEyMC4wOTEgMTM2LjE0NCAxMjAuMTc2IDEzNS41MjMgMTIwLjE3NlpNMTQ2LjkzMiAxMTguMDMxVjEwNi41SDE0OS4wNTlWMTIwSDE0Ny4xMzRMMTQ2LjkzMiAxMTguMDMxWk0xNDAuNzQ0IDExNS4zNTFWMTE1LjE2NkMxNDAuNzQ0IDExNC40NDUgMTQwLjgyOSAxMTMuNzg5IDE0MC45OTkgMTEzLjE5N0MxNDEuMTY5IDExMi42IDE0MS40MTUgMTEyLjA4NyAxNDEuNzM3IDExMS42NTlDMTQyLjA2IDExMS4yMjYgMTQyLjQ1MiAxMTAuODk1IDE0Mi45MTUgMTEwLjY2NkMxNDMuMzc4IDExMC40MzIgMTQzLjg5OSAxMTAuMzE0IDE0NC40NzkgMTEwLjMxNEMxNDUuMDU0IDExMC4zMTQgMTQ1LjU1OCAxMTAuNDI2IDE0NS45OTEgMTEwLjY0OEMxNDYuNDI1IDExMC44NzEgMTQ2Ljc5NCAxMTEuMTkgMTQ3LjA5OSAxMTEuNjA2QzE0Ny40MDMgMTEyLjAxNyAxNDcuNjQ2IDExMi41MDkgMTQ3LjgyOCAxMTMuMDgzQzE0OC4wMSAxMTMuNjUxIDE0OC4xMzkgMTE0LjI4NCAxNDguMjE1IDExNC45ODFWMTE1LjU3QzE0OC4xMzkgMTE2LjI1IDE0OC4wMSAxMTYuODcxIDE0Ny44MjggMTE3LjQzNEMxNDcuNjQ2IDExNy45OTYgMTQ3LjQwMyAxMTguNDgyIDE0Ny4wOTkgMTE4Ljg5M0MxNDYuNzk0IDExOS4zMDMgMTQ2LjQyMiAxMTkuNjE5IDE0NS45ODIgMTE5Ljg0MkMxNDUuNTQ5IDEyMC4wNjQgMTQ1LjA0MiAxMjAuMTc2IDE0NC40NjIgMTIwLjE3NkMxNDMuODg4IDEyMC4xNzYgMTQzLjM2OSAxMjAuMDU2IDE0Mi45MDYgMTE5LjgxNUMxNDIuNDQ5IDExOS41NzUgMTQyLjA2IDExOS4yMzggMTQxLjczNyAxMTguODA1QzE0MS40MTUgMTE4LjM3MSAxNDEuMTY5IDExNy44NjEgMTQwLjk5OSAxMTcuMjc1QzE0MC44MjkgMTE2LjY4NCAxNDAuNzQ0IDExNi4wNDIgMTQwLjc0NCAxMTUuMzUxWk0xNDIuODYyIDExNS4xNjZWMTE1LjM1MUMxNDIuODYyIDExNS43ODQgMTQyLjkgMTE2LjE4OCAxNDIuOTc3IDExNi41NjNDMTQzLjA1OSAxMTYuOTM4IDE0My4xODUgMTE3LjI3IDE0My4zNTQgMTE3LjU1N0MxNDMuNTI0IDExNy44MzggMTQzLjc0NCAxMTguMDYxIDE0NC4wMTQgMTE4LjIyNUMxNDQuMjg5IDExOC4zODMgMTQ0LjYxNyAxMTguNDYyIDE0NC45OTggMTE4LjQ2MkMxNDUuNDc5IDExOC40NjIgMTQ1Ljg3NCAxMTguMzU2IDE0Ni4xODUgMTE4LjE0NkMxNDYuNDk1IDExNy45MzUgMTQ2LjczOCAxMTcuNjUgMTQ2LjkxNCAxMTcuMjkzQzE0Ny4wOTYgMTE2LjkzIDE0Ny4yMTkgMTE2LjUyNSAxNDcuMjgzIDExNi4wOFYxMTQuNDg5QzE0Ny4yNDggMTE0LjE0NCAxNDcuMTc1IDExMy44MjEgMTQ3LjA2MyAxMTMuNTIyQzE0Ni45NTggMTEzLjIyNCAxNDYuODE0IDExMi45NjMgMTQ2LjYzMyAxMTIuNzRDMTQ2LjQ1MSAxMTIuNTEyIDE0Ni4yMjYgMTEyLjMzNiAxNDUuOTU2IDExMi4yMTNDMTQ1LjY5MiAxMTIuMDg0IDE0NS4zNzkgMTEyLjAyIDE0NS4wMTYgMTEyLjAyQzE0NC42MjkgMTEyLjAyIDE0NC4zMDEgMTEyLjEwMiAxNDQuMDMxIDExMi4yNjZDMTQzLjc2MiAxMTIuNDMgMTQzLjUzOSAxMTIuNjU1IDE0My4zNjMgMTEyLjk0MkMxNDMuMTkzIDExMy4yMjkgMTQzLjA2NyAxMTMuNTYzIDE0Mi45ODUgMTEzLjk0NEMxNDIuOTAzIDExNC4zMjUgMTQyLjg2MiAxMTQuNzMyIDE0Mi44NjIgMTE1LjE2NloiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDY5N18zNjcxMyIgeD0iMCIgeT0iMTIiIHdpZHRoPSIyMTYiIGhlaWdodD0iNzYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeT0iNCIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+CjxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4wOCAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzQ2OTdfMzY3MTMiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDY5N18zNjcxMyIgcmVzdWx0PSJzaGFwZSIvPgo8L2ZpbHRlcj4KPC9kZWZzPgo8L3N2Zz4K", + "image": "tb-image:VG9nZ2xlIGJ1dHRvbnMuc3Zn:IlRvZ2dsZSBidXR0b24iIHN5c3RlbSB3aWRnZXQgaW1hZ2U=:SU1BR0U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjE2IiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIxNiAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDY5N18zNjcxMykiPgo8cmVjdCB4PSI4Ljc1IiB5PSIxNi43NSIgd2lkdGg9IjE5OC41IiBoZWlnaHQ9IjU4LjUiIHJ4PSIzLjI1IiBzdHJva2U9IiMxOTgwMzgiIHN0cm9rZS13aWR0aD0iMS41IiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPG1hc2sgaWQ9Im1hc2swXzQ2OTdfMzY3MTMiIHN0eWxlPSJtYXNrLXR5cGU6YWxwaGEiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjYyIiB5PSIzNCIgd2lkdGg9IjI1IiBoZWlnaHQ9IjI0Ij4KPHJlY3QgeD0iNjIuNSIgeT0iMzQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNjguNSA0Mkg3Ny41VjQwQzc3LjUgMzkuMTY2NyA3Ny4yMDgzIDM4LjQ1ODMgNzYuNjI1IDM3Ljg3NUM3Ni4wNDE3IDM3LjI5MTcgNzUuMzMzMyAzNyA3NC41IDM3QzczLjY2NjcgMzcgNzIuOTU4MyAzNy4yOTE3IDcyLjM3NSAzNy44NzVDNzEuNzkxNyAzOC40NTgzIDcxLjUgMzkuMTY2NyA3MS41IDQwSDY5LjVDNjkuNSAzOC42MTY3IDY5Ljk4NzUgMzcuNDM3NSA3MC45NjI1IDM2LjQ2MjVDNzEuOTM3NSAzNS40ODc1IDczLjExNjcgMzUgNzQuNSAzNUM3NS44ODMzIDM1IDc3LjA2MjUgMzUuNDg3NSA3OC4wMzc1IDM2LjQ2MjVDNzkuMDEyNSAzNy40Mzc1IDc5LjUgMzguNjE2NyA3OS41IDQwVjQySDgwLjVDODEuMDUgNDIgODEuNTIwOCA0Mi4xOTU4IDgxLjkxMjUgNDIuNTg3NUM4Mi4zMDQyIDQyLjk3OTIgODIuNSA0My40NSA4Mi41IDQ0VjU0QzgyLjUgNTQuNTUgODIuMzA0MiA1NS4wMjA4IDgxLjkxMjUgNTUuNDEyNUM4MS41MjA4IDU1LjgwNDIgODEuMDUgNTYgODAuNSA1Nkg2OC41QzY3Ljk1IDU2IDY3LjQ3OTIgNTUuODA0MiA2Ny4wODc1IDU1LjQxMjVDNjYuNjk1OCA1NS4wMjA4IDY2LjUgNTQuNTUgNjYuNSA1NFY0NEM2Ni41IDQzLjQ1IDY2LjY5NTggNDIuOTc5MiA2Ny4wODc1IDQyLjU4NzVDNjcuNDc5MiA0Mi4xOTU4IDY3Ljk1IDQyIDY4LjUgNDJaTTY4LjUgNTRIODAuNVY0NEg2OC41VjU0Wk03NC41IDUxQzc1LjA1IDUxIDc1LjUyMDggNTAuODA0MiA3NS45MTI1IDUwLjQxMjVDNzYuMzA0MiA1MC4wMjA4IDc2LjUgNDkuNTUgNzYuNSA0OUM3Ni41IDQ4LjQ1IDc2LjMwNDIgNDcuOTc5MiA3NS45MTI1IDQ3LjU4NzVDNzUuNTIwOCA0Ny4xOTU4IDc1LjA1IDQ3IDc0LjUgNDdDNzMuOTUgNDcgNzMuNDc5MiA0Ny4xOTU4IDczLjA4NzUgNDcuNTg3NUM3Mi42OTU4IDQ3Ljk3OTIgNzIuNSA0OC40NSA3Mi41IDQ5QzcyLjUgNDkuNTUgNzIuNjk1OCA1MC4wMjA4IDczLjA4NzUgNTAuNDEyNUM3My40NzkyIDUwLjgwNDIgNzMuOTUgNTEgNzQuNSA1MVoiIGZpbGw9IiMxOTgwMzgiLz4KPC9nPgo8cGF0aCBkPSJNMTAyLjEzMSA0NS4yNVY0NS45NTMxQzEwMi4xMzEgNDYuOTE5OSAxMDIuMDA1IDQ3Ljc4NzEgMTAxLjc1MyA0OC41NTQ3QzEwMS41MDEgNDkuMzIyMyAxMDEuMTQxIDQ5Ljk3NTYgMTAwLjY3MiA1MC41MTQ2QzEwMC4yMDkgNTEuMDUzNyA5OS42NTIzIDUxLjQ2NjggOTkuMDAyIDUxLjc1MzlDOTguMzUxNiA1Mi4wMzUyIDk3LjYzMDkgNTIuMTc1OCA5Ni44Mzk4IDUyLjE3NThDOTYuMDU0NyA1Mi4xNzU4IDk1LjMzNjkgNTIuMDM1MiA5NC42ODY1IDUxLjc1MzlDOTQuMDQyIDUxLjQ2NjggOTMuNDgyNCA1MS4wNTM3IDkzLjAwNzggNTAuNTE0NkM5Mi41MzMyIDQ5Ljk3NTYgOTIuMTY0MSA0OS4zMjIzIDkxLjkwMDQgNDguNTU0N0M5MS42NDI2IDQ3Ljc4NzEgOTEuNTEzNyA0Ni45MTk5IDkxLjUxMzcgNDUuOTUzMVY0NS4yNUM5MS41MTM3IDQ0LjI4MzIgOTEuNjQyNiA0My40MTg5IDkxLjkwMDQgNDIuNjU3MkM5Mi4xNTgyIDQxLjg4OTYgOTIuNTIxNSA0MS4yMzYzIDkyLjk5MDIgNDAuNjk3M0M5My40NjQ4IDQwLjE1MjMgOTQuMDI0NCAzOS43MzkzIDk0LjY2ODkgMzkuNDU4Qzk1LjMxOTMgMzkuMTcwOSA5Ni4wMzcxIDM5LjAyNzMgOTYuODIyMyAzOS4wMjczQzk3LjYxMzMgMzkuMDI3MyA5OC4zMzQgMzkuMTcwOSA5OC45ODQ0IDM5LjQ1OEM5OS42MzQ4IDM5LjczOTMgMTAwLjE5NCA0MC4xNTIzIDEwMC42NjMgNDAuNjk3M0MxMDEuMTMyIDQxLjIzNjMgMTAxLjQ5MiA0MS44ODk2IDEwMS43NDQgNDIuNjU3MkMxMDIuMDAyIDQzLjQxODkgMTAyLjEzMSA0NC4yODMyIDEwMi4xMzEgNDUuMjVaTTk5LjkyNDggNDUuOTUzMVY0NS4yMzI0Qzk5LjkyNDggNDQuNTE3NiA5OS44NTQ1IDQzLjg4NzcgOTkuNzEzOSA0My4zNDI4Qzk5LjU3OTEgNDIuNzkyIDk5LjM3NyA0Mi4zMzIgOTkuMTA3NCA0MS45NjI5Qzk4Ljg0MzggNDEuNTg3OSA5OC41MTg2IDQxLjMwNjYgOTguMTMxOCA0MS4xMTkxQzk3Ljc0NTEgNDAuOTI1OCA5Ny4zMDg2IDQwLjgyOTEgOTYuODIyMyA0MC44MjkxQzk2LjMzNTkgNDAuODI5MSA5NS45MDIzIDQwLjkyNTggOTUuNTIxNSA0MS4xMTkxQzk1LjE0MDYgNDEuMzA2NiA5NC44MTU0IDQxLjU4NzkgOTQuNTQ1OSA0MS45NjI5Qzk0LjI4MjIgNDIuMzMyIDk0LjA4MDEgNDIuNzkyIDkzLjkzOTUgNDMuMzQyOEM5My43OTg4IDQzLjg4NzcgOTMuNzI4NSA0NC41MTc2IDkzLjcyODUgNDUuMjMyNFY0NS45NTMxQzkzLjcyODUgNDYuNjY4IDkzLjc5ODggNDcuMzAwOCA5My45Mzk1IDQ3Ljg1MTZDOTQuMDgwMSA0OC40MDIzIDk0LjI4NTIgNDguODY4MiA5NC41NTQ3IDQ5LjI0OUM5NC44MzAxIDQ5LjYyNCA5NS4xNTgyIDQ5LjkwODIgOTUuNTM5MSA1MC4xMDE2Qzk1LjkxOTkgNTAuMjg5MSA5Ni4zNTM1IDUwLjM4MjggOTYuODM5OCA1MC4zODI4Qzk3LjMzMiA1MC4zODI4IDk3Ljc2ODYgNTAuMjg5MSA5OC4xNDk0IDUwLjEwMTZDOTguNTMwMyA0OS45MDgyIDk4Ljg1MjUgNDkuNjI0IDk5LjExNjIgNDkuMjQ5Qzk5LjM3OTkgNDguODY4MiA5OS41NzkxIDQ4LjQwMjMgOTkuNzEzOSA0Ny44NTE2Qzk5Ljg1NDUgNDcuMzAwOCA5OS45MjQ4IDQ2LjY2OCA5OS45MjQ4IDQ1Ljk1MzFaTTEwNi40MDMgNDQuMzE4NFY1NS42NTYySDEwNC4yODVWNDIuNDkwMkgxMDYuMjM2TDEwNi40MDMgNDQuMzE4NFpNMTEyLjU5OSA0Ny4xNTcyVjQ3LjM0MThDMTEyLjU5OSA0OC4wMzMyIDExMi41MTcgNDguNjc0OCAxMTIuMzUzIDQ5LjI2NjZDMTEyLjE5NSA0OS44NTI1IDExMS45NTggNTAuMzY1MiAxMTEuNjQxIDUwLjgwNDdDMTExLjMzMSA1MS4yMzgzIDExMC45NDcgNTEuNTc1MiAxMTAuNDkgNTEuODE1NEMxMTAuMDMzIDUyLjA1NTcgMTA5LjUwNSA1Mi4xNzU4IDEwOC45MDggNTIuMTc1OEMxMDguMzE2IDUyLjE3NTggMTA3Ljc5NyA1Mi4wNjc0IDEwNy4zNTIgNTEuODUwNkMxMDYuOTEzIDUxLjYyNzkgMTA2LjU0MSA1MS4zMTQ1IDEwNi4yMzYgNTAuOTEwMkMxMDUuOTMxIDUwLjUwNTkgMTA1LjY4NSA1MC4wMzEyIDEwNS40OTggNDkuNDg2M0MxMDUuMzE2IDQ4LjkzNTUgMTA1LjE4NyA0OC4zMzIgMTA1LjExMSA0Ny42NzU4VjQ2Ljk2MzlDMTA1LjE4NyA0Ni4yNjY2IDEwNS4zMTYgNDUuNjMzOCAxMDUuNDk4IDQ1LjA2NTRDMTA1LjY4NSA0NC40OTcxIDEwNS45MzEgNDQuMDA3OCAxMDYuMjM2IDQzLjU5NzdDMTA2LjU0MSA0My4xODc1IDEwNi45MTMgNDIuODcxMSAxMDcuMzUyIDQyLjY0ODRDMTA3Ljc5MiA0Mi40MjU4IDEwOC4zMDQgNDIuMzE0NSAxMDguODkgNDIuMzE0NUMxMDkuNDg4IDQyLjMxNDUgMTEwLjAxOCA0Mi40MzE2IDExMC40ODEgNDIuNjY2QzExMC45NDQgNDIuODk0NSAxMTEuMzM0IDQzLjIyMjcgMTExLjY1IDQzLjY1MDRDMTExLjk2NiA0NC4wNzIzIDExMi4yMDQgNDQuNTgyIDExMi4zNjIgNDUuMTc5N0MxMTIuNTIgNDUuNzcxNSAxMTIuNTk5IDQ2LjQzMDcgMTEyLjU5OSA0Ny4xNTcyWk0xMTAuNDgxIDQ3LjM0MThWNDcuMTU3MkMxMTAuNDgxIDQ2LjcxNzggMTEwLjQ0IDQ2LjMxMDUgMTEwLjM1OCA0NS45MzU1QzExMC4yNzYgNDUuNTU0NyAxMTAuMTQ3IDQ1LjIyMDcgMTA5Ljk3MSA0NC45MzM2QzEwOS43OTYgNDQuNjQ2NSAxMDkuNTcgNDQuNDIzOCAxMDkuMjk1IDQ0LjI2NTZDMTA5LjAyNSA0NC4xMDE2IDEwOC43IDQ0LjAxOTUgMTA4LjMxOSA0NC4wMTk1QzEwNy45NDQgNDQuMDE5NSAxMDcuNjIyIDQ0LjA4NCAxMDcuMzUyIDQ0LjIxMjlDMTA3LjA4MyA0NC4zMzU5IDEwNi44NTcgNDQuNTA4OCAxMDYuNjc1IDQ0LjczMTRDMTA2LjQ5NCA0NC45NTQxIDEwNi4zNTMgNDUuMjE0OCAxMDYuMjU0IDQ1LjUxMzdDMTA2LjE1NCA0NS44MDY2IDEwNi4wODQgNDYuMTI2IDEwNi4wNDMgNDYuNDcxN1Y0OC4xNzY4QzEwNi4xMTMgNDguNTk4NiAxMDYuMjMzIDQ4Ljk4NTQgMTA2LjQwMyA0OS4zMzY5QzEwNi41NzMgNDkuNjg4NSAxMDYuODEzIDQ5Ljk2OTcgMTA3LjEyNCA1MC4xODA3QzEwNy40NCA1MC4zODU3IDEwNy44NDQgNTAuNDg4MyAxMDguMzM3IDUwLjQ4ODNDMTA4LjcxNyA1MC40ODgzIDEwOS4wNDMgNTAuNDA2MiAxMDkuMzEyIDUwLjI0MjJDMTA5LjU4MiA1MC4wNzgxIDEwOS44MDEgNDkuODUyNSAxMDkuOTcxIDQ5LjU2NTRDMTEwLjE0NyA0OS4yNzI1IDExMC4yNzYgNDguOTM1NSAxMTAuMzU4IDQ4LjU1NDdDMTEwLjQ0IDQ4LjE3MzggMTEwLjQ4MSA0Ny43Njk1IDExMC40ODEgNDcuMzQxOFpNMTE4Ljc0MyA1Mi4xNzU4QzExOC4wNCA1Mi4xNzU4IDExNy40MDQgNTIuMDYxNSAxMTYuODM2IDUxLjgzM0MxMTYuMjc0IDUxLjU5ODYgMTE1Ljc5MyA1MS4yNzM0IDExNS4zOTUgNTAuODU3NEMxMTUuMDAyIDUwLjQ0MTQgMTE0LjcgNDkuOTUyMSAxMTQuNDg5IDQ5LjM4OTZDMTE0LjI3OSA0OC44MjcxIDExNC4xNzMgNDguMjIwNyAxMTQuMTczIDQ3LjU3MDNWNDcuMjE4OEMxMTQuMTczIDQ2LjQ3NDYgMTE0LjI4MSA0NS44MDA4IDExNC40OTggNDUuMTk3M0MxMTQuNzE1IDQ0LjU5MzggMTE1LjAxNyA0NC4wNzgxIDExNS40MDQgNDMuNjUwNEMxMTUuNzkgNDMuMjE2OCAxMTYuMjQ3IDQyLjg4NTcgMTE2Ljc3NSA0Mi42NTcyQzExNy4zMDIgNDIuNDI4NyAxMTcuODczIDQyLjMxNDUgMTE4LjQ4OCA0Mi4zMTQ1QzExOS4xNjggNDIuMzE0NSAxMTkuNzYzIDQyLjQyODcgMTIwLjI3MyA0Mi42NTcyQzEyMC43ODIgNDIuODg1NyAxMjEuMjA0IDQzLjIwOCAxMjEuNTM4IDQzLjYyNEMxMjEuODc4IDQ0LjAzNDIgMTIyLjEzIDQ0LjUyMzQgMTIyLjI5NCA0NS4wOTE4QzEyMi40NjQgNDUuNjYwMiAxMjIuNTQ5IDQ2LjI4NzEgMTIyLjU0OSA0Ni45NzI3VjQ3Ljg3NzlIMTE1LjIwMVY0Ni4zNTc0SDEyMC40NTdWNDYuMTkwNEMxMjAuNDQ2IDQ1LjgwOTYgMTIwLjM2OSA0NS40NTIxIDEyMC4yMjkgNDUuMTE4MkMxMjAuMDk0IDQ0Ljc4NDIgMTE5Ljg4NiA0NC41MTQ2IDExOS42MDUgNDQuMzA5NkMxMTkuMzIzIDQ0LjEwNDUgMTE4Ljk0OCA0NC4wMDIgMTE4LjQ4IDQ0LjAwMkMxMTguMTI4IDQ0LjAwMiAxMTcuODE1IDQ0LjA3ODEgMTE3LjUzOSA0NC4yMzA1QzExNy4yNyA0NC4zNzcgMTE3LjA0NCA0NC41OTA4IDExNi44NjIgNDQuODcyMUMxMTYuNjgxIDQ1LjE1MzMgMTE2LjU0IDQ1LjQ5MzIgMTE2LjQ0MSA0NS44OTE2QzExNi4zNDcgNDYuMjg0MiAxMTYuMyA0Ni43MjY2IDExNi4zIDQ3LjIxODhWNDcuNTcwM0MxMTYuMyA0Ny45ODYzIDExNi4zNTYgNDguMzczIDExNi40NjcgNDguNzMwNUMxMTYuNTg0IDQ5LjA4MiAxMTYuNzU0IDQ5LjM4OTYgMTE2Ljk3NyA0OS42NTMzQzExNy4xOTkgNDkuOTE3IDExNy40NjkgNTAuMTI1IDExNy43ODUgNTAuMjc3M0MxMTguMTAyIDUwLjQyMzggMTE4LjQ2MiA1MC40OTcxIDExOC44NjYgNTAuNDk3MUMxMTkuMzc2IDUwLjQ5NzEgMTE5LjgzIDUwLjM5NDUgMTIwLjIyOSA1MC4xODk1QzEyMC42MjcgNDkuOTg0NCAxMjAuOTczIDQ5LjY5NDMgMTIxLjI2NiA0OS4zMTkzTDEyMi4zODIgNTAuNDAwNEMxMjIuMTc3IDUwLjY5OTIgMTIxLjkxIDUwLjk4NjMgMTIxLjU4MiA1MS4yNjE3QzEyMS4yNTQgNTEuNTMxMiAxMjAuODUzIDUxLjc1MSAxMjAuMzc4IDUxLjkyMDlDMTE5LjkwOSA1Mi4wOTA4IDExOS4zNjQgNTIuMTc1OCAxMTguNzQzIDUyLjE3NThaTTEyNi40NTIgNDQuNTIwNVY1MkgxMjQuMzM0VjQyLjQ5MDJIMTI2LjMyOUwxMjYuNDUyIDQ0LjUyMDVaTTEyNi4wNzQgNDYuODkzNkwxMjUuMzg4IDQ2Ljg4NDhDMTI1LjM5NCA0Ni4yMTA5IDEyNS40ODggNDUuNTkyOCAxMjUuNjcgNDUuMDMwM0MxMjUuODU3IDQ0LjQ2NzggMTI2LjExNSA0My45ODQ0IDEyNi40NDMgNDMuNTgwMUMxMjYuNzc3IDQzLjE3NTggMTI3LjE3NiA0Mi44NjUyIDEyNy42MzggNDIuNjQ4NEMxMjguMTAxIDQyLjQyNTggMTI4LjYxNyA0Mi4zMTQ1IDEyOS4xODUgNDIuMzE0NUMxMjkuNjQyIDQyLjMxNDUgMTMwLjA1NSA0Mi4zNzg5IDEzMC40MjUgNDIuNTA3OEMxMzAuOCA0Mi42MzA5IDEzMS4xMTkgNDIuODMzIDEzMS4zODMgNDMuMTE0M0MxMzEuNjUyIDQzLjM5NTUgMTMxLjg1NyA0My43NjE3IDEzMS45OTggNDQuMjEyOUMxMzIuMTM4IDQ0LjY1ODIgMTMyLjIwOSA0NS4yMDYxIDEzMi4yMDkgNDUuODU2NFY1MkgxMzAuMDgyVjQ1Ljg0NzdDMTMwLjA4MiA0NS4zOTA2IDEzMC4wMTQgNDUuMDMwMyAxMjkuODggNDQuNzY2NkMxMjkuNzUxIDQ0LjQ5NzEgMTI5LjU2IDQ0LjMwNjYgMTI5LjMwOCA0NC4xOTUzQzEyOS4wNjIgNDQuMDc4MSAxMjguNzU1IDQ0LjAxOTUgMTI4LjM4NiA0NC4wMTk1QzEyOC4wMjIgNDQuMDE5NSAxMjcuNjk3IDQ0LjA5NTcgMTI3LjQxIDQ0LjI0OEMxMjcuMTIzIDQ0LjQwMDQgMTI2Ljg4IDQ0LjYwODQgMTI2LjY4IDQ0Ljg3MjFDMTI2LjQ4NyA0NS4xMzU3IDEyNi4zMzggNDUuNDQwNCAxMjYuMjMyIDQ1Ljc4NjFDMTI2LjEyNyA0Ni4xMzE4IDEyNi4wNzQgNDYuNTAxIDEyNi4wNzQgNDYuODkzNlpNMTM4LjcxMyA1Mi4xNzU4QzEzOC4wMSA1Mi4xNzU4IDEzNy4zNzQgNTIuMDYxNSAxMzYuODA2IDUxLjgzM0MxMzYuMjQ0IDUxLjU5ODYgMTM1Ljc2MyA1MS4yNzM0IDEzNS4zNjUgNTAuODU3NEMxMzQuOTcyIDUwLjQ0MTQgMTM0LjY3IDQ5Ljk1MjEgMTM0LjQ1OSA0OS4zODk2QzEzNC4yNDggNDguODI3MSAxMzQuMTQzIDQ4LjIyMDcgMTM0LjE0MyA0Ny41NzAzVjQ3LjIxODhDMTM0LjE0MyA0Ni40NzQ2IDEzNC4yNTEgNDUuODAwOCAxMzQuNDY4IDQ1LjE5NzNDMTM0LjY4NSA0NC41OTM4IDEzNC45ODcgNDQuMDc4MSAxMzUuMzczIDQzLjY1MDRDMTM1Ljc2IDQzLjIxNjggMTM2LjIxNyA0Mi44ODU3IDEzNi43NDUgNDIuNjU3MkMxMzcuMjcyIDQyLjQyODcgMTM3Ljg0MyA0Mi4zMTQ1IDEzOC40NTggNDIuMzE0NUMxMzkuMTM4IDQyLjMxNDUgMTM5LjczMyA0Mi40Mjg3IDE0MC4yNDMgNDIuNjU3MkMxNDAuNzUyIDQyLjg4NTcgMTQxLjE3NCA0My4yMDggMTQxLjUwOCA0My42MjRDMTQxLjg0OCA0NC4wMzQyIDE0Mi4xIDQ0LjUyMzQgMTQyLjI2NCA0NS4wOTE4QzE0Mi40MzQgNDUuNjYwMiAxNDIuNTE5IDQ2LjI4NzEgMTQyLjUxOSA0Ni45NzI3VjQ3Ljg3NzlIMTM1LjE3MVY0Ni4zNTc0SDE0MC40MjdWNDYuMTkwNEMxNDAuNDE1IDQ1LjgwOTYgMTQwLjMzOSA0NS40NTIxIDE0MC4xOTkgNDUuMTE4MkMxNDAuMDY0IDQ0Ljc4NDIgMTM5Ljg1NiA0NC41MTQ2IDEzOS41NzUgNDQuMzA5NkMxMzkuMjkzIDQ0LjEwNDUgMTM4LjkxOCA0NC4wMDIgMTM4LjQ1IDQ0LjAwMkMxMzguMDk4IDQ0LjAwMiAxMzcuNzg1IDQ0LjA3ODEgMTM3LjUwOSA0NC4yMzA1QzEzNy4yNCA0NC4zNzcgMTM3LjAxNCA0NC41OTA4IDEzNi44MzIgNDQuODcyMUMxMzYuNjUxIDQ1LjE1MzMgMTM2LjUxIDQ1LjQ5MzIgMTM2LjQxMSA0NS44OTE2QzEzNi4zMTcgNDYuMjg0MiAxMzYuMjcgNDYuNzI2NiAxMzYuMjcgNDcuMjE4OFY0Ny41NzAzQzEzNi4yNyA0Ny45ODYzIDEzNi4zMjYgNDguMzczIDEzNi40MzcgNDguNzMwNUMxMzYuNTU0IDQ5LjA4MiAxMzYuNzI0IDQ5LjM4OTYgMTM2Ljk0NyA0OS42NTMzQzEzNy4xNjkgNDkuOTE3IDEzNy40MzkgNTAuMTI1IDEzNy43NTUgNTAuMjc3M0MxMzguMDcyIDUwLjQyMzggMTM4LjQzMiA1MC40OTcxIDEzOC44MzYgNTAuNDk3MUMxMzkuMzQ2IDUwLjQ5NzEgMTM5LjggNTAuMzk0NSAxNDAuMTk5IDUwLjE4OTVDMTQwLjU5NyA0OS45ODQ0IDE0MC45NDMgNDkuNjk0MyAxNDEuMjM2IDQ5LjMxOTNMMTQyLjM1MiA1MC40MDA0QzE0Mi4xNDcgNTAuNjk5MiAxNDEuODggNTAuOTg2MyAxNDEuNTUyIDUxLjI2MTdDMTQxLjIyNCA1MS41MzEyIDE0MC44MjMgNTEuNzUxIDE0MC4zNDggNTEuOTIwOUMxMzkuODc5IDUyLjA5MDggMTM5LjMzNCA1Mi4xNzU4IDEzOC43MTMgNTIuMTc1OFpNMTUwLjEyMiA1MC4wMzEyVjM4LjVIMTUyLjI0OVY1MkgxNTAuMzI0TDE1MC4xMjIgNTAuMDMxMlpNMTQzLjkzNSA0Ny4zNTA2VjQ3LjE2NkMxNDMuOTM1IDQ2LjQ0NTMgMTQ0LjAyIDQ1Ljc4OTEgMTQ0LjE4OSA0NS4xOTczQzE0NC4zNTkgNDQuNTk5NiAxNDQuNjA1IDQ0LjA4NjkgMTQ0LjkyOCA0My42NTkyQzE0NS4yNSA0My4yMjU2IDE0NS42NDMgNDIuODk0NSAxNDYuMTA1IDQyLjY2NkMxNDYuNTY4IDQyLjQzMTYgMTQ3LjA5IDQyLjMxNDUgMTQ3LjY3IDQyLjMxNDVDMTQ4LjI0NCA0Mi4zMTQ1IDE0OC43NDggNDIuNDI1OCAxNDkuMTgyIDQyLjY0ODRDMTQ5LjYxNSA0Mi44NzExIDE0OS45ODQgNDMuMTkwNCAxNTAuMjg5IDQzLjYwNjRDMTUwLjU5NCA0NC4wMTY2IDE1MC44MzcgNDQuNTA4OCAxNTEuMDE5IDQ1LjA4M0MxNTEuMiA0NS42NTE0IDE1MS4zMjkgNDYuMjg0MiAxNTEuNDA1IDQ2Ljk4MTRWNDcuNTcwM0MxNTEuMzI5IDQ4LjI1IDE1MS4yIDQ4Ljg3MTEgMTUxLjAxOSA0OS40MzM2QzE1MC44MzcgNDkuOTk2MSAxNTAuNTk0IDUwLjQ4MjQgMTUwLjI4OSA1MC44OTI2QzE0OS45ODQgNTEuMzAyNyAxNDkuNjEyIDUxLjYxOTEgMTQ5LjE3MyA1MS44NDE4QzE0OC43MzkgNTIuMDY0NSAxNDguMjMyIDUyLjE3NTggMTQ3LjY1MiA1Mi4xNzU4QzE0Ny4wNzggNTIuMTc1OCAxNDYuNTYgNTIuMDU1NyAxNDYuMDk3IDUxLjgxNTRDMTQ1LjY0IDUxLjU3NTIgMTQ1LjI1IDUxLjIzODMgMTQ0LjkyOCA1MC44MDQ3QzE0NC42MDUgNTAuMzcxMSAxNDQuMzU5IDQ5Ljg2MTMgMTQ0LjE4OSA0OS4yNzU0QzE0NC4wMiA0OC42ODM2IDE0My45MzUgNDguMDQyIDE0My45MzUgNDcuMzUwNlpNMTQ2LjA1MyA0Ny4xNjZWNDcuMzUwNkMxNDYuMDUzIDQ3Ljc4NDIgMTQ2LjA5MSA0OC4xODg1IDE0Ni4xNjcgNDguNTYzNUMxNDYuMjQ5IDQ4LjkzODUgMTQ2LjM3NSA0OS4yNjk1IDE0Ni41NDUgNDkuNTU2NkMxNDYuNzE1IDQ5LjgzNzkgMTQ2LjkzNSA1MC4wNjA1IDE0Ny4yMDQgNTAuMjI0NkMxNDcuNDc5IDUwLjM4MjggMTQ3LjgwOCA1MC40NjE5IDE0OC4xODggNTAuNDYxOUMxNDguNjY5IDUwLjQ2MTkgMTQ5LjA2NCA1MC4zNTY0IDE0OS4zNzUgNTAuMTQ1NUMxNDkuNjg2IDQ5LjkzNDYgMTQ5LjkyOSA0OS42NTA0IDE1MC4xMDQgNDkuMjkzQzE1MC4yODYgNDguOTI5NyAxNTAuNDA5IDQ4LjUyNTQgMTUwLjQ3NCA0OC4wODAxVjQ2LjQ4OTNDMTUwLjQzOCA0Ni4xNDM2IDE1MC4zNjUgNDUuODIxMyAxNTAuMjU0IDQ1LjUyMjVDMTUwLjE0OCA0NS4yMjM2IDE1MC4wMDUgNDQuOTYyOSAxNDkuODIzIDQ0Ljc0MDJDMTQ5LjY0MiA0NC41MTE3IDE0OS40MTYgNDQuMzM1OSAxNDkuMTQ2IDQ0LjIxMjlDMTQ4Ljg4MyA0NC4wODQgMTQ4LjU2OSA0NC4wMTk1IDE0OC4yMDYgNDQuMDE5NUMxNDcuODE5IDQ0LjAxOTUgMTQ3LjQ5MSA0NC4xMDE2IDE0Ny4yMjIgNDQuMjY1NkMxNDYuOTUyIDQ0LjQyOTcgMTQ2LjcyOSA0NC42NTUzIDE0Ni41NTQgNDQuOTQyNEMxNDYuMzg0IDQ1LjIyOTUgMTQ2LjI1OCA0NS41NjM1IDE0Ni4xNzYgNDUuOTQ0M0MxNDYuMDk0IDQ2LjMyNTIgMTQ2LjA1MyA0Ni43MzI0IDE0Ni4wNTMgNDcuMTY2WiIgZmlsbD0iIzE5ODAzOCIvPgo8L2c+CjxtYXNrIGlkPSJwYXRoLTYtaW5zaWRlLTFfNDY5N18zNjcxMyIgZmlsbD0id2hpdGUiPgo8cGF0aCBkPSJNOCA4OEM4IDg1Ljc5MDkgOS43OTA4NiA4NCAxMiA4NEgyMDRDMjA2LjIwOSA4NCAyMDggODUuNzkwOSAyMDggODhWMTQwQzIwOCAxNDIuMjA5IDIwNi4yMDkgMTQ0IDIwNCAxNDRIMTJDOS43OTA4NiAxNDQgOCAxNDIuMjA5IDggMTQwVjg4WiIvPgo8L21hc2s+CjxwYXRoIGQ9Ik04IDg4QzggODUuNzkwOSA5Ljc5MDg2IDg0IDEyIDg0SDIwNEMyMDYuMjA5IDg0IDIwOCA4NS43OTA5IDIwOCA4OFYxNDBDMjA4IDE0Mi4yMDkgMjA2LjIwOSAxNDQgMjA0IDE0NEgxMkM5Ljc5MDg2IDE0NCA4IDE0Mi4yMDkgOCAxNDBWODhaIiBmaWxsPSIjRDEyNzMwIi8+CjxwYXRoIGQ9Ik04IDg0SDIwOEg4Wk0yMDggMTQwQzIwOCAxNDMuMzE0IDIwNS4zMTQgMTQ2IDIwMiAxNDZIMTRDMTAuNjg2MyAxNDYgOCAxNDMuMzE0IDggMTQwQzggMTQxLjEwNSA5Ljc5MDg2IDE0MiAxMiAxNDJIMjA0QzIwNi4yMDkgMTQyIDIwOCAxNDEuMTA1IDIwOCAxNDBaTTggMTQ0Vjg0VjE0NFpNMjA4IDg0VjE0NFY4NFoiIGZpbGw9IiNEMTI3MzAiIG1hc2s9InVybCgjcGF0aC02LWluc2lkZS0xXzQ2OTdfMzY3MTMpIi8+CjxtYXNrIGlkPSJtYXNrMV80Njk3XzM2NzEzIiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSI2NSIgeT0iMTAyIiB3aWR0aD0iMjUiIGhlaWdodD0iMjQiPgo8cmVjdCB4PSI2NS41IiB5PSIxMDIiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazFfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNzEuNSAxMjRDNzAuOTUgMTI0IDcwLjQ3OTIgMTIzLjgwNCA3MC4wODc1IDEyMy40MTNDNjkuNjk1OCAxMjMuMDIxIDY5LjUgMTIyLjU1IDY5LjUgMTIyVjExMkM2OS41IDExMS40NSA2OS42OTU4IDExMC45NzkgNzAuMDg3NSAxMTAuNTg4QzcwLjQ3OTIgMTEwLjE5NiA3MC45NSAxMTAgNzEuNSAxMTBINzIuNVYxMDhDNzIuNSAxMDYuNjE3IDcyLjk4NzUgMTA1LjQzOCA3My45NjI1IDEwNC40NjJDNzQuOTM3NSAxMDMuNDg3IDc2LjExNjcgMTAzIDc3LjUgMTAzQzc4Ljg4MzMgMTAzIDgwLjA2MjUgMTAzLjQ4NyA4MS4wMzc1IDEwNC40NjJDODIuMDEyNSAxMDUuNDM4IDgyLjUgMTA2LjYxNyA4Mi41IDEwOFYxMTBIODMuNUM4NC4wNSAxMTAgODQuNTIwOCAxMTAuMTk2IDg0LjkxMjUgMTEwLjU4OEM4NS4zMDQyIDExMC45NzkgODUuNSAxMTEuNDUgODUuNSAxMTJWMTIyQzg1LjUgMTIyLjU1IDg1LjMwNDIgMTIzLjAyMSA4NC45MTI1IDEyMy40MTNDODQuNTIwOCAxMjMuODA0IDg0LjA1IDEyNCA4My41IDEyNEg3MS41Wk03MS41IDEyMkg4My41VjExMkg3MS41VjEyMlpNNzcuNSAxMTlDNzguMDUgMTE5IDc4LjUyMDggMTE4LjgwNCA3OC45MTI1IDExOC40MTNDNzkuMzA0MiAxMTguMDIxIDc5LjUgMTE3LjU1IDc5LjUgMTE3Qzc5LjUgMTE2LjQ1IDc5LjMwNDIgMTE1Ljk3OSA3OC45MTI1IDExNS41ODhDNzguNTIwOCAxMTUuMTk2IDc4LjA1IDExNSA3Ny41IDExNUM3Ni45NSAxMTUgNzYuNDc5MiAxMTUuMTk2IDc2LjA4NzUgMTE1LjU4OEM3NS42OTU4IDExNS45NzkgNzUuNSAxMTYuNDUgNzUuNSAxMTdDNzUuNSAxMTcuNTUgNzUuNjk1OCAxMTguMDIxIDc2LjA4NzUgMTE4LjQxM0M3Ni40NzkyIDExOC44MDQgNzYuOTUgMTE5IDc3LjUgMTE5Wk03NC41IDExMEg4MC41VjEwOEM4MC41IDEwNy4xNjcgODAuMjA4MyAxMDYuNDU4IDc5LjYyNSAxMDUuODc1Qzc5LjA0MTcgMTA1LjI5MiA3OC4zMzMzIDEwNSA3Ny41IDEwNUM3Ni42NjY3IDEwNSA3NS45NTgzIDEwNS4yOTIgNzUuMzc1IDEwNS44NzVDNzQuNzkxNyAxMDYuNDU4IDc0LjUgMTA3LjE2NyA3NC41IDEwOFYxMTBaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxwYXRoIGQ9Ik0xMDIuNjc2IDExNS44MzRIMTA0Ljg3M0MxMDQuODAzIDExNi42NzIgMTA0LjU2OCAxMTcuNDE5IDEwNC4xNyAxMTguMDc1QzEwMy43NzEgMTE4LjcyNiAxMDMuMjEyIDExOS4yMzggMTAyLjQ5MSAxMTkuNjEzQzEwMS43NzEgMTE5Ljk4OCAxMDAuODk1IDEyMC4xNzYgOTkuODYzMyAxMjAuMTc2Qzk5LjA3MjMgMTIwLjE3NiA5OC4zNjA0IDEyMC4wMzUgOTcuNzI3NSAxMTkuNzU0Qzk3LjA5NDcgMTE5LjQ2NyA5Ni41NTI3IDExOS4wNjIgOTYuMTAxNiAxMTguNTQxQzk1LjY1MDQgMTE4LjAxNCA5NS4zMDQ3IDExNy4zNzggOTUuMDY0NSAxMTYuNjM0Qzk0LjgzMDEgMTE1Ljg5IDk0LjcxMjkgMTE1LjA1OCA5NC43MTI5IDExNC4xMzhWMTEzLjA3NEM5NC43MTI5IDExMi4xNTQgOTQuODMzIDExMS4zMjIgOTUuMDczMiAxMTAuNTc4Qzk1LjMxOTMgMTA5LjgzNCA5NS42NzA5IDEwOS4xOTggOTYuMTI3OSAxMDguNjcxQzk2LjU4NSAxMDguMTM4IDk3LjEzMjggMTA3LjczIDk3Ljc3MTUgMTA3LjQ0OUM5OC40MTYgMTA3LjE2OCA5OS4xMzk2IDEwNy4wMjcgOTkuOTQyNCAxMDcuMDI3QzEwMC45NjIgMTA3LjAyNyAxMDEuODIzIDEwNy4yMTUgMTAyLjUyNiAxMDcuNTlDMTAzLjIyOSAxMDcuOTY1IDEwMy43NzQgMTA4LjQ4MyAxMDQuMTYxIDEwOS4xNDZDMTA0LjU1NCAxMDkuODA4IDEwNC43OTQgMTEwLjU2NiAxMDQuODgyIDExMS40MjJIMTAyLjY4NUMxMDIuNjI2IDExMC44NzEgMTAyLjQ5NyAxMTAuMzk5IDEwMi4yOTggMTEwLjAwN0MxMDIuMTA0IDEwOS42MTQgMTAxLjgxNyAxMDkuMzE1IDEwMS40MzcgMTA5LjExQzEwMS4wNTYgMTA4Ljg5OSAxMDAuNTU4IDEwOC43OTQgOTkuOTQyNCAxMDguNzk0Qzk5LjQzODUgMTA4Ljc5NCA5OC45OTkgMTA4Ljg4OCA5OC42MjQgMTA5LjA3NUM5OC4yNDkgMTA5LjI2MyA5Ny45MzU1IDEwOS41MzggOTcuNjgzNiAxMDkuOTAxQzk3LjQzMTYgMTEwLjI2NSA5Ny4yNDEyIDExMC43MTMgOTcuMTEyMyAxMTEuMjQ2Qzk2Ljk4OTMgMTExLjc3MyA5Ni45Mjc3IDExMi4zNzcgOTYuOTI3NyAxMTMuMDU3VjExNC4xMzhDOTYuOTI3NyAxMTQuNzgyIDk2Ljk4MzQgMTE1LjM2OCA5Ny4wOTQ3IDExNS44OTZDOTcuMjExOSAxMTYuNDE3IDk3LjM4NzcgMTE2Ljg2NSA5Ny42MjIxIDExNy4yNEM5Ny44NjIzIDExNy42MTUgOTguMTY3IDExNy45MDUgOTguNTM2MSAxMTguMTFDOTguOTA1MyAxMTguMzE1IDk5LjM0NzcgMTE4LjQxOCA5OS44NjMzIDExOC40MThDMTAwLjQ5IDExOC40MTggMTAwLjk5NyAxMTguMzE4IDEwMS4zODQgMTE4LjExOUMxMDEuNzc2IDExNy45MiAxMDIuMDcyIDExNy42MyAxMDIuMjcxIDExNy4yNDlDMTAyLjQ3NyAxMTYuODYyIDEwMi42MTEgMTE2LjM5MSAxMDIuNjc2IDExNS44MzRaTTEwOS4wODQgMTA2LjVWMTIwSDEwNi45NTdWMTA2LjVIMTA5LjA4NFpNMTExLjE1IDExNS4zNTFWMTE1LjE0OEMxMTEuMTUgMTE0LjQ2MyAxMTEuMjQ5IDExMy44MjcgMTExLjQ0OCAxMTMuMjQxQzExMS42NDggMTEyLjY0OSAxMTEuOTM1IDExMi4xMzcgMTEyLjMxIDExMS43MDNDMTEyLjY5MSAxMTEuMjY0IDExMy4xNTQgMTEwLjkyNCAxMTMuNjk4IDExMC42ODRDMTE0LjI0OSAxMTAuNDM4IDExNC44NyAxMTAuMzE0IDExNS41NjIgMTEwLjMxNEMxMTYuMjU5IDExMC4zMTQgMTE2Ljg4IDExMC40MzggMTE3LjQyNSAxMTAuNjg0QzExNy45NzYgMTEwLjkyNCAxMTguNDQyIDExMS4yNjQgMTE4LjgyMiAxMTEuNzAzQzExOS4yMDMgMTEyLjEzNyAxMTkuNDkzIDExMi42NDkgMTE5LjY5MyAxMTMuMjQxQzExOS44OTIgMTEzLjgyNyAxMTkuOTkxIDExNC40NjMgMTE5Ljk5MSAxMTUuMTQ4VjExNS4zNTFDMTE5Ljk5MSAxMTYuMDM2IDExOS44OTIgMTE2LjY3MiAxMTkuNjkzIDExNy4yNThDMTE5LjQ5MyAxMTcuODQ0IDExOS4yMDMgMTE4LjM1NiAxMTguODIyIDExOC43OTZDMTE4LjQ0MiAxMTkuMjI5IDExNy45NzkgMTE5LjU2OSAxMTcuNDM0IDExOS44MTVDMTE2Ljg4OSAxMjAuMDU2IDExNi4yNzEgMTIwLjE3NiAxMTUuNTc5IDEyMC4xNzZDMTE0Ljg4MiAxMjAuMTc2IDExNC4yNTggMTIwLjA1NiAxMTMuNzA3IDExOS44MTVDMTEzLjE2MiAxMTkuNTY5IDExMi42OTkgMTE5LjIyOSAxMTIuMzE5IDExOC43OTZDMTExLjkzOCAxMTguMzU2IDExMS42NDggMTE3Ljg0NCAxMTEuNDQ4IDExNy4yNThDMTExLjI0OSAxMTYuNjcyIDExMS4xNSAxMTYuMDM2IDExMS4xNSAxMTUuMzUxWk0xMTMuMjY4IDExNS4xNDhWMTE1LjM1MUMxMTMuMjY4IDExNS43NzggMTEzLjMxMiAxMTYuMTgzIDExMy40IDExNi41NjNDMTEzLjQ4NyAxMTYuOTQ0IDExMy42MjUgMTE3LjI3OCAxMTMuODEzIDExNy41NjVDMTE0IDExNy44NTMgMTE0LjI0IDExOC4wNzggMTE0LjUzMyAxMTguMjQyQzExNC44MjYgMTE4LjQwNiAxMTUuMTc1IDExOC40ODggMTE1LjU3OSAxMTguNDg4QzExNS45NzIgMTE4LjQ4OCAxMTYuMzEyIDExOC40MDYgMTE2LjU5OSAxMTguMjQyQzExNi44OTIgMTE4LjA3OCAxMTcuMTMyIDExNy44NTMgMTE3LjMyIDExNy41NjVDMTE3LjUwNyAxMTcuMjc4IDExNy42NDUgMTE2Ljk0NCAxMTcuNzMzIDExNi41NjNDMTE3LjgyNiAxMTYuMTgzIDExNy44NzMgMTE1Ljc3OCAxMTcuODczIDExNS4zNTFWMTE1LjE0OEMxMTcuODczIDExNC43MjcgMTE3LjgyNiAxMTQuMzI4IDExNy43MzMgMTEzLjk1M0MxMTcuNjQ1IDExMy41NzIgMTE3LjUwNCAxMTMuMjM1IDExNy4zMTEgMTEyLjk0MkMxMTcuMTIzIDExMi42NDkgMTE2Ljg4MyAxMTIuNDIxIDExNi41OSAxMTIuMjU3QzExNi4zMDMgMTEyLjA4NyAxMTUuOTYgMTEyLjAwMiAxMTUuNTYyIDExMi4wMDJDMTE1LjE2MyAxMTIuMDAyIDExNC44MTggMTEyLjA4NyAxMTQuNTI1IDExMi4yNTdDMTE0LjIzNyAxMTIuNDIxIDExNCAxMTIuNjQ5IDExMy44MTMgMTEyLjk0MkMxMTMuNjI1IDExMy4yMzUgMTEzLjQ4NyAxMTMuNTcyIDExMy40IDExMy45NTNDMTEzLjMxMiAxMTQuMzI4IDExMy4yNjggMTE0LjcyNyAxMTMuMjY4IDExNS4xNDhaTTEyNy4yNTIgMTE3LjQyNUMxMjcuMjUyIDExNy4yMTQgMTI3LjE5OSAxMTcuMDIzIDEyNy4wOTQgMTE2Ljg1NEMxMjYuOTg4IDExNi42NzggMTI2Ljc4NiAxMTYuNTIgMTI2LjQ4NyAxMTYuMzc5QzEyNi4xOTQgMTE2LjIzOCAxMjUuNzYxIDExNi4xMDkgMTI1LjE4NiAxMTUuOTkyQzEyNC42ODIgMTE1Ljg4MSAxMjQuMjIgMTE1Ljc0OSAxMjMuNzk4IDExNS41OTdDMTIzLjM4MiAxMTUuNDM4IDEyMy4wMjQgMTE1LjI0OCAxMjIuNzI1IDExNS4wMjVDMTIyLjQyNyAxMTQuODAzIDEyMi4xOTUgMTE0LjUzOSAxMjIuMDMxIDExNC4yMzRDMTIxLjg2NyAxMTMuOTMgMTIxLjc4NSAxMTMuNTc4IDEyMS43ODUgMTEzLjE4QzEyMS43ODUgMTEyLjc5MyAxMjEuODcgMTEyLjQyNyAxMjIuMDQgMTEyLjA4MUMxMjIuMjEgMTExLjczNSAxMjIuNDUzIDExMS40MzEgMTIyLjc2OSAxMTEuMTY3QzEyMy4wODYgMTEwLjkwMyAxMjMuNDcgMTEwLjY5NSAxMjMuOTIxIDExMC41NDNDMTI0LjM3OCAxMTAuMzkxIDEyNC44ODggMTEwLjMxNCAxMjUuNDUgMTEwLjMxNEMxMjYuMjQ3IDExMC4zMTQgMTI2LjkyOSAxMTAuNDQ5IDEyNy40OTggMTEwLjcxOUMxMjguMDcyIDExMC45ODIgMTI4LjUxMiAxMTEuMzQzIDEyOC44MTYgMTExLjhDMTI5LjEyMSAxMTIuMjUxIDEyOS4yNzMgMTEyLjc2MSAxMjkuMjczIDExMy4zMjlIMTI3LjE1NUMxMjcuMTU1IDExMy4wNzcgMTI3LjA5MSAxMTIuODQzIDEyNi45NjIgMTEyLjYyNkMxMjYuODM5IDExMi40MDMgMTI2LjY1MSAxMTIuMjI1IDEyNi4zOTkgMTEyLjA5QzEyNi4xNDcgMTExLjk0OSAxMjUuODMxIDExMS44NzkgMTI1LjQ1IDExMS44NzlDMTI1LjA4NyAxMTEuODc5IDEyNC43ODUgMTExLjkzOCAxMjQuNTQ1IDExMi4wNTVDMTI0LjMxIDExMi4xNjYgMTI0LjEzNSAxMTIuMzEyIDEyNC4wMTcgMTEyLjQ5NEMxMjMuOTA2IDExMi42NzYgMTIzLjg1IDExMi44NzUgMTIzLjg1IDExMy4wOTJDMTIzLjg1IDExMy4yNSAxMjMuODggMTEzLjM5NCAxMjMuOTM4IDExMy41MjJDMTI0LjAwMyAxMTMuNjQ2IDEyNC4xMDggMTEzLjc2IDEyNC4yNTUgMTEzLjg2NUMxMjQuNDAxIDExMy45NjUgMTI0LjYgMTE0LjA1OSAxMjQuODUyIDExNC4xNDZDMTI1LjExIDExNC4yMzQgMTI1LjQzMiAxMTQuMzE5IDEyNS44MTkgMTE0LjQwMUMxMjYuNTQ2IDExNC41NTQgMTI3LjE3IDExNC43NSAxMjcuNjkxIDExNC45OUMxMjguMjE5IDExNS4yMjUgMTI4LjYyMyAxMTUuNTI5IDEyOC45MDQgMTE1LjkwNEMxMjkuMTg1IDExNi4yNzMgMTI5LjMyNiAxMTYuNzQyIDEyOS4zMjYgMTE3LjMxMUMxMjkuMzI2IDExNy43MzIgMTI5LjIzNSAxMTguMTE5IDEyOS4wNTQgMTE4LjQ3MUMxMjguODc4IDExOC44MTYgMTI4LjYyIDExOS4xMTggMTI4LjI4IDExOS4zNzZDMTI3Ljk0IDExOS42MjggMTI3LjUzMyAxMTkuODI0IDEyNy4wNTggMTE5Ljk2NUMxMjYuNTkgMTIwLjEwNSAxMjYuMDYyIDEyMC4xNzYgMTI1LjQ3NiAxMjAuMTc2QzEyNC42MTUgMTIwLjE3NiAxMjMuODg2IDEyMC4wMjMgMTIzLjI4OCAxMTkuNzE5QzEyMi42OSAxMTkuNDA4IDEyMi4yMzYgMTE5LjAxMyAxMjEuOTI2IDExOC41MzJDMTIxLjYyMSAxMTguMDQ2IDEyMS40NjkgMTE3LjU0MiAxMjEuNDY5IDExNy4wMjFIMTIzLjUxNkMxMjMuNTQgMTE3LjQxMyAxMjMuNjQ4IDExNy43MjcgMTIzLjg0MiAxMTcuOTYxQzEyNC4wNDEgMTE4LjE4OSAxMjQuMjg3IDExOC4zNTYgMTI0LjU4IDExOC40NjJDMTI0Ljg3OSAxMTguNTYyIDEyNS4xODYgMTE4LjYxMSAxMjUuNTAzIDExOC42MTFDMTI1Ljg4NCAxMTguNjExIDEyNi4yMDMgMTE4LjU2MiAxMjYuNDYxIDExOC40NjJDMTI2LjcxOSAxMTguMzU2IDEyNi45MTUgMTE4LjIxNiAxMjcuMDUgMTE4LjA0QzEyNy4xODQgMTE3Ljg1OCAxMjcuMjUyIDExNy42NTMgMTI3LjI1MiAxMTcuNDI1Wk0xMzUuNTIzIDEyMC4xNzZDMTM0LjgyIDEyMC4xNzYgMTM0LjE4NCAxMjAuMDYyIDEzMy42MTYgMTE5LjgzM0MxMzMuMDUzIDExOS41OTkgMTMyLjU3MyAxMTkuMjczIDEzMi4xNzQgMTE4Ljg1N0MxMzEuNzgyIDExOC40NDEgMTMxLjQ4IDExNy45NTIgMTMxLjI2OSAxMTcuMzlDMTMxLjA1OCAxMTYuODI3IDEzMC45NTMgMTE2LjIyMSAxMzAuOTUzIDExNS41N1YxMTUuMjE5QzEzMC45NTMgMTE0LjQ3NSAxMzEuMDYxIDExMy44MDEgMTMxLjI3OCAxMTMuMTk3QzEzMS40OTUgMTEyLjU5NCAxMzEuNzk2IDExMi4wNzggMTMyLjE4MyAxMTEuNjVDMTMyLjU3IDExMS4yMTcgMTMzLjAyNyAxMTAuODg2IDEzMy41NTQgMTEwLjY1N0MxMzQuMDgxIDExMC40MjkgMTM0LjY1MyAxMTAuMzE0IDEzNS4yNjggMTEwLjMxNEMxMzUuOTQ4IDExMC4zMTQgMTM2LjU0MiAxMTAuNDI5IDEzNy4wNTIgMTEwLjY1N0MxMzcuNTYyIDExMC44ODYgMTM3Ljk4NCAxMTEuMjA4IDEzOC4zMTggMTExLjYyNEMxMzguNjU4IDExMi4wMzQgMTM4LjkxIDExMi41MjMgMTM5LjA3NCAxMTMuMDkyQzEzOS4yNDQgMTEzLjY2IDEzOS4zMjkgMTE0LjI4NyAxMzkuMzI5IDExNC45NzNWMTE1Ljg3OEgxMzEuOTgxVjExNC4zNTdIMTM3LjIzN1YxMTQuMTlDMTM3LjIyNSAxMTMuODEgMTM3LjE0OSAxMTMuNDUyIDEzNy4wMDggMTEzLjExOEMxMzYuODczIDExMi43ODQgMTM2LjY2NSAxMTIuNTE1IDEzNi4zODQgMTEyLjMxQzEzNi4xMDMgMTEyLjEwNCAxMzUuNzI4IDExMi4wMDIgMTM1LjI1OSAxMTIuMDAyQzEzNC45MDggMTEyLjAwMiAxMzQuNTk0IDExMi4wNzggMTM0LjMxOSAxMTIuMjNDMTM0LjA0OSAxMTIuMzc3IDEzMy44MjQgMTEyLjU5MSAxMzMuNjQyIDExMi44NzJDMTMzLjQ2IDExMy4xNTMgMTMzLjMyIDExMy40OTMgMTMzLjIyIDExMy44OTJDMTMzLjEyNiAxMTQuMjg0IDEzMy4wNzkgMTE0LjcyNyAxMzMuMDc5IDExNS4yMTlWMTE1LjU3QzEzMy4wNzkgMTE1Ljk4NiAxMzMuMTM1IDExNi4zNzMgMTMzLjI0NiAxMTYuNzNDMTMzLjM2NCAxMTcuMDgyIDEzMy41MzQgMTE3LjM5IDEzMy43NTYgMTE3LjY1M0MxMzMuOTc5IDExNy45MTcgMTM0LjI0OCAxMTguMTI1IDEzNC41NjUgMTE4LjI3N0MxMzQuODgxIDExOC40MjQgMTM1LjI0MiAxMTguNDk3IDEzNS42NDYgMTE4LjQ5N0MxMzYuMTU2IDExOC40OTcgMTM2LjYxIDExOC4zOTUgMTM3LjAwOCAxMTguMTg5QzEzNy40MDcgMTE3Ljk4NCAxMzcuNzUyIDExNy42OTQgMTM4LjA0NSAxMTcuMzE5TDEzOS4xNjIgMTE4LjRDMTM4Ljk1NiAxMTguNjk5IDEzOC42OSAxMTguOTg2IDEzOC4zNjIgMTE5LjI2MkMxMzguMDM0IDExOS41MzEgMTM3LjYzMiAxMTkuNzUxIDEzNy4xNTggMTE5LjkyMUMxMzYuNjg5IDEyMC4wOTEgMTM2LjE0NCAxMjAuMTc2IDEzNS41MjMgMTIwLjE3NlpNMTQ2LjkzMiAxMTguMDMxVjEwNi41SDE0OS4wNTlWMTIwSDE0Ny4xMzRMMTQ2LjkzMiAxMTguMDMxWk0xNDAuNzQ0IDExNS4zNTFWMTE1LjE2NkMxNDAuNzQ0IDExNC40NDUgMTQwLjgyOSAxMTMuNzg5IDE0MC45OTkgMTEzLjE5N0MxNDEuMTY5IDExMi42IDE0MS40MTUgMTEyLjA4NyAxNDEuNzM3IDExMS42NTlDMTQyLjA2IDExMS4yMjYgMTQyLjQ1MiAxMTAuODk1IDE0Mi45MTUgMTEwLjY2NkMxNDMuMzc4IDExMC40MzIgMTQzLjg5OSAxMTAuMzE0IDE0NC40NzkgMTEwLjMxNEMxNDUuMDU0IDExMC4zMTQgMTQ1LjU1OCAxMTAuNDI2IDE0NS45OTEgMTEwLjY0OEMxNDYuNDI1IDExMC44NzEgMTQ2Ljc5NCAxMTEuMTkgMTQ3LjA5OSAxMTEuNjA2QzE0Ny40MDMgMTEyLjAxNyAxNDcuNjQ2IDExMi41MDkgMTQ3LjgyOCAxMTMuMDgzQzE0OC4wMSAxMTMuNjUxIDE0OC4xMzkgMTE0LjI4NCAxNDguMjE1IDExNC45ODFWMTE1LjU3QzE0OC4xMzkgMTE2LjI1IDE0OC4wMSAxMTYuODcxIDE0Ny44MjggMTE3LjQzNEMxNDcuNjQ2IDExNy45OTYgMTQ3LjQwMyAxMTguNDgyIDE0Ny4wOTkgMTE4Ljg5M0MxNDYuNzk0IDExOS4zMDMgMTQ2LjQyMiAxMTkuNjE5IDE0NS45ODIgMTE5Ljg0MkMxNDUuNTQ5IDEyMC4wNjQgMTQ1LjA0MiAxMjAuMTc2IDE0NC40NjIgMTIwLjE3NkMxNDMuODg4IDEyMC4xNzYgMTQzLjM2OSAxMjAuMDU2IDE0Mi45MDYgMTE5LjgxNUMxNDIuNDQ5IDExOS41NzUgMTQyLjA2IDExOS4yMzggMTQxLjczNyAxMTguODA1QzE0MS40MTUgMTE4LjM3MSAxNDEuMTY5IDExNy44NjEgMTQwLjk5OSAxMTcuMjc1QzE0MC44MjkgMTE2LjY4NCAxNDAuNzQ0IDExNi4wNDIgMTQwLjc0NCAxMTUuMzUxWk0xNDIuODYyIDExNS4xNjZWMTE1LjM1MUMxNDIuODYyIDExNS43ODQgMTQyLjkgMTE2LjE4OCAxNDIuOTc3IDExNi41NjNDMTQzLjA1OSAxMTYuOTM4IDE0My4xODUgMTE3LjI3IDE0My4zNTQgMTE3LjU1N0MxNDMuNTI0IDExNy44MzggMTQzLjc0NCAxMTguMDYxIDE0NC4wMTQgMTE4LjIyNUMxNDQuMjg5IDExOC4zODMgMTQ0LjYxNyAxMTguNDYyIDE0NC45OTggMTE4LjQ2MkMxNDUuNDc5IDExOC40NjIgMTQ1Ljg3NCAxMTguMzU2IDE0Ni4xODUgMTE4LjE0NkMxNDYuNDk1IDExNy45MzUgMTQ2LjczOCAxMTcuNjUgMTQ2LjkxNCAxMTcuMjkzQzE0Ny4wOTYgMTE2LjkzIDE0Ny4yMTkgMTE2LjUyNSAxNDcuMjgzIDExNi4wOFYxMTQuNDg5QzE0Ny4yNDggMTE0LjE0NCAxNDcuMTc1IDExMy44MjEgMTQ3LjA2MyAxMTMuNTIyQzE0Ni45NTggMTEzLjIyNCAxNDYuODE0IDExMi45NjMgMTQ2LjYzMyAxMTIuNzRDMTQ2LjQ1MSAxMTIuNTEyIDE0Ni4yMjYgMTEyLjMzNiAxNDUuOTU2IDExMi4yMTNDMTQ1LjY5MiAxMTIuMDg0IDE0NS4zNzkgMTEyLjAyIDE0NS4wMTYgMTEyLjAyQzE0NC42MjkgMTEyLjAyIDE0NC4zMDEgMTEyLjEwMiAxNDQuMDMxIDExMi4yNjZDMTQzLjc2MiAxMTIuNDMgMTQzLjUzOSAxMTIuNjU1IDE0My4zNjMgMTEyLjk0MkMxNDMuMTkzIDExMy4yMjkgMTQzLjA2NyAxMTMuNTYzIDE0Mi45ODUgMTEzLjk0NEMxNDIuOTAzIDExNC4zMjUgMTQyLjg2MiAxMTQuNzMyIDE0Mi44NjIgMTE1LjE2NloiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDY5N18zNjcxMyIgeD0iMCIgeT0iMTIiIHdpZHRoPSIyMTYiIGhlaWdodD0iNzYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeT0iNCIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+CjxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4wOCAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzQ2OTdfMzY3MTMiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDY5N18zNjcxMyIgcmVzdWx0PSJzaGFwZSIvPgo8L2ZpbHRlcj4KPC9kZWZzPgo8L3N2Zz4K", "description": "Sends the command to the device or updates attribute/time series when the user toggles the button. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when button checked/unchecked states.", "descriptor": { "type": "rpc", @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '150px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '150px',\n embedTitlePanel: true,\n overflowVisible: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-toggle-button-widget-settings", diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql index afbd86fee7..51e78a504e 100644 --- a/application/src/main/data/upgrade/3.7.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql @@ -14,6 +14,55 @@ -- limitations under the License. -- +-- UPDATE RESOURCE SUB TYPE START + +DO +$$ + BEGIN + IF NOT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'resource' AND column_name = 'resource_sub_type' + ) THEN + ALTER TABLE resource ADD COLUMN resource_sub_type varchar(32); + UPDATE resource SET resource_sub_type = 'IMAGE' WHERE resource_type = 'IMAGE'; + END IF; + END; +$$; + +-- UPDATE RESOURCE SUB TYPE END + +-- UPDATE WIDGETS BUNDLE START + +DO +$$ + BEGIN + IF NOT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'widgets_bundle' AND column_name = 'scada' + ) THEN + ALTER TABLE widgets_bundle ADD COLUMN scada boolean NOT NULL DEFAULT false; + END IF; + END; +$$; + +-- UPDATE WIDGETS BUNDLE END + +-- UPDATE WIDGET TYPE START + +DO +$$ + BEGIN + IF NOT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'widget_type' AND column_name = 'scada' + ) THEN + ALTER TABLE widget_type ADD COLUMN scada boolean NOT NULL DEFAULT false; + END IF; + END; +$$; + +-- UPDATE WIDGET TYPE END + -- KV VERSIONING UPDATE START CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1; diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 011119886d..4d83d4debb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -117,6 +117,8 @@ public class ControllerConstants { protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. "; protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. "; + protected static final String RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION = "A string value representing resource sub-type."; + protected static final String RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION = "Use 'true' to include system images. Disabled by default. Ignored for requests by users with system administrator authority."; protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title."; diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index e8e7ca1d8f..6ebb6fffe8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -41,6 +41,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ImageExportData; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbResource; @@ -65,6 +66,7 @@ import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; @@ -96,7 +98,8 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PostMapping(value = "/api/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public TbResourceInfo uploadImage(@RequestPart MultipartFile file, - @RequestPart(required = false) String title) throws Exception { + @RequestPart(required = false) String title, + @RequestPart(required = false) String imageSubType) throws Exception { SecurityUser user = getCurrentUser(); TbResource image = new TbResource(); image.setTenantId(user.getTenantId()); @@ -109,7 +112,14 @@ public class ImageController extends BaseController { } else { image.setTitle(file.getOriginalFilename()); } + + ResourceSubType subType = ResourceSubType.IMAGE; + if (StringUtils.isNotEmpty(imageSubType)) { + subType = ResourceSubType.valueOf(imageSubType); + } + image.setResourceType(ResourceType.IMAGE); + image.setResourceSubType(subType); ImageDescriptor descriptor = new ImageDescriptor(); descriptor.setMediaType(file.getContentType()); image.setDescriptorValue(descriptor); @@ -194,6 +204,7 @@ public class ImageController extends BaseController { .mediaType(descriptor.getMediaType()) .fileName(imageInfo.getFileName()) .title(imageInfo.getTitle()) + .subType(imageInfo.getResourceSubType().name()) .resourceKey(imageInfo.getResourceKey()) .isPublic(imageInfo.isPublic()) .publicResourceKey(imageInfo.getPublicResourceKey()) @@ -215,6 +226,11 @@ public class ImageController extends BaseController { } else { image.setTitle(imageData.getFileName()); } + if (StringUtils.isNotEmpty(imageData.getSubType())) { + image.setResourceSubType(ResourceSubType.valueOf(imageData.getSubType())); + } else { + image.setResourceSubType(ResourceSubType.IMAGE); + } image.setResourceType(ResourceType.IMAGE); image.setResourceKey(imageData.getResourceKey()); image.setPublic(imageData.isPublic()); @@ -251,6 +267,8 @@ public class ImageController extends BaseController { @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"IMAGE", "SCADA_SYMBOL"})) + @RequestParam(required = false) String imageSubType, @Parameter(description = RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION) @RequestParam(required = false) boolean includeSystemImages, @Parameter(description = RESOURCE_TEXT_SEARCH_DESCRIPTION) @@ -262,10 +280,14 @@ public class ImageController extends BaseController { // PE: generic permission PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); TenantId tenantId = getTenantId(); + ResourceSubType subType = ResourceSubType.IMAGE; + if (StringUtils.isNotEmpty(imageSubType)) { + subType = ResourceSubType.valueOf(imageSubType); + } if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN || !includeSystemImages) { - return checkNotNull(imageService.getImagesByTenantId(tenantId, pageLink)); + return checkNotNull(imageService.getImagesByTenantId(tenantId, subType, pageLink)); } else { - return checkNotNull(imageService.getAllImagesByTenantId(tenantId, pageLink)); + return checkNotNull(imageService.getAllImagesByTenantId(tenantId, subType, pageLink)); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index 649a58f22e..03f1c52faf 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -41,8 +41,10 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.DeprecatedFilter; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.resource.ImageService; @@ -83,6 +85,7 @@ public class WidgetTypeController extends AutoCommitController { private static final String WIDGET_TYPE_INFO_DESCRIPTION = "Widget Type Info is a lightweight object that represents Widget Type but does not contain the heavyweight widget descriptor JSON"; private static final String TENANT_ONLY_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether only tenant widget types should be returned"; private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether search widgets by description not only by name"; + private static final String SCADA_FIRST_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to fetch SCADA symbol widgets first"; private static final String DEPRECATED_FILTER_PARAM_DESCRIPTION = "Optional string parameter indicating whether to include deprecated widgets"; private static final String UPDATE_EXISTING_BY_FQN_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to update existing widget type by FQN if present instead of creating new one"; private static final String WIDGET_TYPE_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the widget type value"; @@ -186,18 +189,26 @@ public class WidgetTypeController extends AutoCommitController { @Parameter(description = DEPRECATED_FILTER_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"ALL", "ACTUAL", "DEPRECATED"})) @RequestParam(required = false) String deprecatedFilter, @Parameter(description = WIDGET_TYPE_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"timeseries", "latest", "control", "alarm", "static"}))) - @RequestParam(required = false) String[] widgetTypeList) throws ThingsboardException { + @RequestParam(required = false) String[] widgetTypeList, + @Parameter(description = SCADA_FIRST_PARAM_DESCRIPTION) + @RequestParam(required = false) Boolean scadaFirst) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); List widgetTypes = widgetTypeList != null ? Arrays.asList(widgetTypeList) : Collections.emptyList(); - boolean fullSearchBool = fullSearch != null && fullSearch; DeprecatedFilter widgetTypeDeprecatedFilter = StringUtils.isNotEmpty(deprecatedFilter) ? DeprecatedFilter.valueOf(deprecatedFilter) : DeprecatedFilter.ALL; + WidgetTypeFilter widgetTypeFilter = WidgetTypeFilter.builder() + .tenantId(getTenantId()) + .widgetTypes(widgetTypes) + .deprecatedFilter(widgetTypeDeprecatedFilter) + .fullSearch(fullSearch != null && fullSearch) + .scadaFirst(scadaFirst != null && scadaFirst) + .build(); if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { - return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink)); + return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(widgetTypeFilter, pageLink)); } else { if (tenantOnly != null && tenantOnly) { - return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink)); + return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(widgetTypeFilter, pageLink)); } else { - return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink)); + return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(widgetTypeFilter, pageLink)); } } } diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index ecd63561f4..8d39e6c83d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -72,6 +73,7 @@ public class WidgetsBundleController extends BaseController { private static final String WIDGET_BUNDLE_DESCRIPTION = "Widget Bundle represents a group(bundle) of widgets. Widgets are grouped into bundle by type or use case. "; private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating extended search of widget bundles by description and by name / description of related widget types"; + private static final String SCADA_FIRST_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to fetch widgets bundles with SCADA symbols first. Works only when fullSearch parameter is enabled"; private static final String TENANT_BUNDLES_ONLY_DESCRIPTION = "Optional boolean parameter to include only tenant-level bundles without system"; @ApiOperation(value = "Get Widget Bundle (getWidgetsBundleById)", @@ -198,16 +200,22 @@ public class WidgetsBundleController extends BaseController { @Parameter(description = TENANT_BUNDLES_ONLY_DESCRIPTION) @RequestParam(required = false) Boolean tenantOnly, @Parameter(description = FULL_SEARCH_PARAM_DESCRIPTION) - @RequestParam(required = false) Boolean fullSearch) throws ThingsboardException { + @RequestParam(required = false) Boolean fullSearch, + @Parameter(description = SCADA_FIRST_PARAM_DESCRIPTION) + @RequestParam(required = false) Boolean scadaFirst) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + WidgetsBundleFilter widgetsBundleFilter = WidgetsBundleFilter.builder() + .tenantId(getTenantId()) + .fullSearch(fullSearch != null && fullSearch) + .scadaFirst(scadaFirst != null && scadaFirst) + .build(); if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { - return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink)); + return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(widgetsBundleFilter, pageLink)); } else { - TenantId tenantId = getCurrentUser().getTenantId(); if (tenantOnly != null && tenantOnly) { - return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); + return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(widgetsBundleFilter, pageLink)); } else { - return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); + return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(widgetsBundleFilter, pageLink)); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java index 7e957fd0fe..2cf47c6abd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.DeprecatedFilter; +import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.dao.widget.WidgetTypeService; @@ -32,6 +33,12 @@ public class SystemWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventF @Override protected PageData findWidgetTypes(TenantId tenantId, PageLink pageLink) { - return widgetTypeService.findSystemWidgetTypesByPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink); + return widgetTypeService.findSystemWidgetTypesByPageLink( + WidgetTypeFilter.builder() + .tenantId(tenantId) + .fullSearch(false) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), + pageLink); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java index 5ec2af5fb2..1e6a345db7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.dao.widget.WidgetsBundleService; @Slf4j @@ -31,6 +32,6 @@ public class SystemWidgetsBundlesEdgeEventFetcher extends BaseWidgetsBundlesEdge @Override protected PageData findWidgetsBundles(TenantId tenantId, PageLink pageLink) { - return widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink); + return widgetsBundleService.findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java index c2a971a0c0..62fac80fbf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.DeprecatedFilter; +import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.dao.widget.WidgetTypeService; @@ -31,6 +32,12 @@ public class TenantWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventF } @Override protected PageData findWidgetTypes(TenantId tenantId, PageLink pageLink) { - return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink); + return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink( + WidgetTypeFilter.builder() + .tenantId(tenantId) + .fullSearch(false) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), + pageLink); } } 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..7f76ce860f 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); } @@ -253,6 +265,7 @@ public class InstallScripts { ); } } + this.loadSystemScadaSymbols(); for (var widgetsBundleDescriptorEntry : widgetsBundlesMap.entrySet()) { Path path = widgetsBundleDescriptorEntry.getKey(); try { @@ -290,6 +303,94 @@ public class InstallScripts { } } + private 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 scadaSymbolWidgetTemplate = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "scada_symbol"); + 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 (scadaSymbolWidgetTemplate != null) { + saveScadaSymbolWidget(scadaSymbolWidgetTemplate, 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); + } + } + ); + } + } + } + + 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 { + var existing = imageService.getImageInfoByTenantIdAndKey(TenantId.SYS_TENANT_ID, fileName); + if (existing != null && ResourceSubType.SCADA_SYMBOL.equals(existing.getResourceSubType())) { + imageService.deleteImage(existing, true); + } + 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 WidgetTypeDetails 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()); + scadaSymbolWidget.setScada(true); + 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)); + ((ObjectNode)descriptor).put("sizeX", metadata.getWidgetSizeX()); + ((ObjectNode)descriptor).put("sizeY", metadata.getWidgetSizeY()); + String controllerScript = descriptor.get("controllerScript").asText(); + controllerScript = controllerScript.replaceAll("previewWidth: '\\d*px'", "previewWidth: '" + (metadata.getWidgetSizeX() * 100) + "px'"); + controllerScript = controllerScript.replaceAll("previewHeight: '\\d*px'", "previewHeight: '" + (metadata.getWidgetSizeY() * 100 + 20) + "px'"); + ((ObjectNode)descriptor).put("controllerScript", controllerScript); + return widgetTypeService.saveWidgetType(scadaSymbolWidget); + } + private void deleteSystemWidgetBundle(String bundleAlias) { WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias); if (widgetsBundle != null) { diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java index e272a40857..29bf048082 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java @@ -23,11 +23,14 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockPart; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ImageExportData; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.SystemParams; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.page.PageData; @@ -35,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.sql.resource.TbResourceRepository; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; @@ -66,6 +70,8 @@ public class ImageControllerTest extends AbstractControllerTest { String filename = "my_png_image.png"; TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE); + assertThat(imageInfo.getTitle()).isEqualTo(filename); assertThat(imageInfo.getResourceType()).isEqualTo(ResourceType.IMAGE); assertThat(imageInfo.getResourceKey()).isEqualTo(filename); @@ -85,6 +91,8 @@ public class ImageControllerTest extends AbstractControllerTest { String filename = "my_jpeg_image.jpg"; TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE); + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); checkJpegImageDescriptor(imageDescriptor); @@ -97,6 +105,22 @@ public class ImageControllerTest extends AbstractControllerTest { String filename = "my_svg_image.svg"; TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/svg+xml", SVG_IMAGE); + assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE); + + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); + checkSvgImageDescriptor(imageDescriptor); + + assertThat(downloadImage("tenant", filename)).containsExactly(SVG_IMAGE); + assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize()); + } + + @Test + public void testUploadScadaSymbolImage() throws Exception { + String filename = "my_scada_symbol_image.svg"; + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.SCADA_SYMBOL.name(), filename, "image/svg+xml", SVG_IMAGE); + + assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.SCADA_SYMBOL); + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); checkSvgImageDescriptor(imageDescriptor); @@ -175,6 +199,7 @@ public class ImageControllerTest extends AbstractControllerTest { assertThat(exportData.getMediaType()).isEqualTo("image/png"); assertThat(exportData.getFileName()).isEqualTo(filename); assertThat(exportData.getTitle()).isEqualTo(filename); + assertThat(exportData.getSubType()).isEqualTo(ResourceSubType.IMAGE.name()); assertThat(exportData.getResourceKey()).isEqualTo(filename); assertThat(exportData.getData()).isEqualTo(Base64.getEncoder().encodeToString(PNG_IMAGE)); assertThat(exportData.isPublic()).isTrue(); @@ -184,6 +209,7 @@ public class ImageControllerTest extends AbstractControllerTest { TbResourceInfo importedImageInfo = doPut("/api/image/import", exportData, TbResourceInfo.class); assertThat(importedImageInfo.getTitle()).isEqualTo(filename); + assertThat(exportData.getSubType()).isEqualTo(ResourceSubType.IMAGE.name()); assertThat(importedImageInfo.getResourceKey()).isEqualTo(filename); assertThat(importedImageInfo.getFileName()).isEqualTo(filename); assertThat(importedImageInfo.isPublic()).isTrue(); @@ -198,20 +224,37 @@ public class ImageControllerTest extends AbstractControllerTest { String systemImageName = "my_system_png_image.png"; TbResourceInfo systemImage = uploadImage(HttpMethod.POST, "/api/image", systemImageName, "image/png", PNG_IMAGE); + String systemScadaSymbolName = "my_system_scada_symbol_image.svg"; + TbResourceInfo systemScadaSymbol = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.SCADA_SYMBOL.name(), systemScadaSymbolName, "image/svg+xml", SVG_IMAGE); + loginTenantAdmin(); String tenantImageName = "my_jpeg_image.jpg"; TbResourceInfo tenantImage = uploadImage(HttpMethod.POST, "/api/image", tenantImageName, "image/jpeg", JPEG_IMAGE); + String tenantScadaSymbolName = "my_scada_symbol_image.svg"; + TbResourceInfo tenantScadaSymbol = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.SCADA_SYMBOL.name(), tenantScadaSymbolName, "image/svg+xml", SVG_IMAGE); + List tenantImages = getImages(null, false, 10); assertThat(tenantImages).containsOnly(tenantImage); + List tenantScadaSymbols = getImages(null, ResourceSubType.SCADA_SYMBOL.name(), false, 10); + assertThat(tenantScadaSymbols).containsOnly(tenantScadaSymbol); + List allImages = getImages(null, true, 10); assertThat(allImages).containsOnly(tenantImage, systemImage); + List allScadaSymbols = getImages(null, ResourceSubType.SCADA_SYMBOL.name(), true, 10); + assertThat(allScadaSymbols).containsOnly(tenantScadaSymbol, systemScadaSymbol); + assertThat(getImages("png", true, 10)) .containsOnly(systemImage); assertThat(getImages("jpg", true, 10)) .containsOnly(tenantImage); + + assertThat(getImages("my_system_scada_symbol", ResourceSubType.SCADA_SYMBOL.name(), true, 10)) + .containsOnly(systemScadaSymbol); + assertThat(getImages("my_scada_symbol", ResourceSubType.SCADA_SYMBOL.name(),true, 10)) + .containsOnly(tenantScadaSymbol); } @Test @@ -312,7 +355,15 @@ public class ImageControllerTest extends AbstractControllerTest { } private List getImages(String searchText, boolean includeSystemImages, int limit) throws Exception { - PageData images = doGetTypedWithPageLink("/api/images?includeSystemImages=" + includeSystemImages + "&", new TypeReference<>() {}, new PageLink(limit, 0, searchText)); + return this.getImages(searchText, null, includeSystemImages, limit); + } + + private List getImages(String searchText, String imageSubType, boolean includeSystemImages, int limit) throws Exception { + var url = "/api/images?includeSystemImages=" + includeSystemImages + "&"; + if (StringUtils.isNotEmpty(imageSubType)) { + url += "imageSubType=" + imageSubType+ "&"; + } + PageData images = doGetTypedWithPageLink(url, new TypeReference<>() {}, new PageLink(limit, 0, searchText)); return images.getData(); } @@ -332,8 +383,16 @@ public class ImageControllerTest extends AbstractControllerTest { } private TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content) throws Exception { + return this.uploadImage(httpMethod, url, null, filename, mediaType, content); + } + + private TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String subType, String filename, String mediaType, byte[] content) throws Exception { MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content); var request = MockMvcRequestBuilders.multipart(httpMethod, url).file(file); + if (StringUtils.isNotEmpty(subType)) { + var imageSubTypePart = new MockPart("imageSubType", subType.getBytes(StandardCharsets.UTF_8)); + request.part(imageSubTypePart); + } setJwtToken(request); return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class); } diff --git a/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java index 2d6009b15c..37b50fda4f 100644 --- a/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.dao.dashboard.DashboardService; 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.service.validator.RuleChainDataValidator; @@ -65,6 +66,8 @@ class InstallScriptsTest { @MockBean ResourceService resourceService; @MockBean + ImageService imageService; + @MockBean ImagesUpdater imagesUpdater; @SpyBean InstallScripts installScripts; 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 9a65fcfdae..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 @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.HasImage; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -36,9 +37,9 @@ public interface ImageService { TbResourceInfo getPublicImageInfoByKey(String publicResourceKey); - PageData getImagesByTenantId(TenantId tenantId, PageLink pageLink); + PageData getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink); - PageData getAllImagesByTenantId(TenantId tenantId, PageLink pageLink); + PageData getAllImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink); byte[] getImageData(TenantId tenantId, TbResourceId imageId); @@ -46,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..39a5210849 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 @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.DeprecatedFilter; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.dao.entity.EntityDaoService; @@ -40,11 +41,11 @@ public interface WidgetTypeService extends EntityDaoService { void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId); - PageData findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); + PageData findSystemWidgetTypesByPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink); - PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); + PageData findAllTenantWidgetTypesByTenantIdAndPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink); - PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); + PageData findTenantWidgetTypesByTenantIdAndPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink); List findWidgetTypesByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId); @@ -56,6 +57,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/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java index 396ba4e3fa..9dbc0149a9 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; @@ -34,15 +35,15 @@ public interface WidgetsBundleService extends EntityDaoService { WidgetsBundle findWidgetsBundleByTenantIdAndAlias(TenantId tenantId, String alias); - PageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); + PageData findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink); List findSystemWidgetsBundles(TenantId tenantId); PageData findTenantWidgetsBundlesByTenantId(TenantId tenantId, PageLink pageLink); - PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); + PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink); - PageData findTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); + PageData findTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink); List findAllTenantWidgetsBundlesByTenantId(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java index b407563d24..3be7b6f92d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java @@ -33,6 +33,7 @@ public class ImageExportData { private String mediaType; private String fileName; private String title; + private String subType; private String resourceKey; private boolean isPublic; private String publicResourceKey; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java new file mode 100644 index 0000000000..10a033f75d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java @@ -0,0 +1,21 @@ +/** + * 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. + */ +package org.thingsboard.server.common.data; + +public enum ResourceSubType { + IMAGE, + SCADA_SYMBOL +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index f1318c2257..8537fe68ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -46,6 +46,8 @@ public class TbResourceInfo extends BaseData implements HasName, H private String title; @Schema(description = "Resource type.", example = "LWM2M_MODEL", accessMode = Schema.AccessMode.READ_ONLY) private ResourceType resourceType; + @Schema(description = "Resource sub type.", example = "IOT_SVG", accessMode = Schema.AccessMode.READ_ONLY) + private ResourceSubType resourceSubType; @NoXss @Length(fieldName = "resourceKey") @Schema(description = "Resource key.", example = "19_1.0", accessMode = Schema.AccessMode.READ_ONLY) @@ -78,6 +80,7 @@ public class TbResourceInfo extends BaseData implements HasName, H this.tenantId = resourceInfo.tenantId; this.title = resourceInfo.title; this.resourceType = resourceInfo.resourceType; + this.resourceSubType = resourceInfo.resourceSubType; this.resourceKey = resourceInfo.resourceKey; this.searchText = resourceInfo.searchText; this.isPublic = resourceInfo.isPublic; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java index 87d9b69749..60583dbb96 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java @@ -27,5 +27,6 @@ public class TbResourceInfoFilter { private TenantId tenantId; private Set resourceTypes; + private Set resourceSubTypes; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java index 6e1198d840..36692f5738 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java @@ -47,6 +47,9 @@ public class BaseWidgetType extends BaseData implements HasName, H @Schema(description = "Whether widget type is deprecated.", example = "true") private boolean deprecated; + @Schema(description = "Whether widget type is SCADA symbol.", example = "true") + private boolean scada; + private Long version; public BaseWidgetType() { @@ -63,6 +66,7 @@ public class BaseWidgetType extends BaseData implements HasName, H this.fqn = widgetType.getFqn(); this.name = widgetType.getName(); this.deprecated = widgetType.isDeprecated(); + this.scada = widgetType.isScada(); this.version = widgetType.getVersion(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeFilter.java new file mode 100644 index 0000000000..e8d1601b64 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeFilter.java @@ -0,0 +1,34 @@ +/** + * 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. + */ +package org.thingsboard.server.common.data.widget; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; + +@Data +@Builder +public class WidgetTypeFilter { + + private TenantId tenantId; + private boolean fullSearch; + private boolean scadaFirst; + DeprecatedFilter deprecatedFilter; + List widgetTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java index e8e964a033..005e4ca252 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java @@ -62,6 +62,11 @@ public class WidgetsBundle extends BaseData implements HasName, @Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.", accessMode = Schema.AccessMode.READ_ONLY) private String image; + @Getter + @Setter + @Schema(description = "Whether widgets bundle contains SCADA symbol widget types.", accessMode = Schema.AccessMode.READ_ONLY) + private boolean scada; + @NoXss @Length(fieldName = "description", max = 1024) @Getter @@ -95,6 +100,7 @@ public class WidgetsBundle extends BaseData implements HasName, this.alias = widgetsBundle.getAlias(); this.title = widgetsBundle.getTitle(); this.image = widgetsBundle.getImage(); + this.scada = widgetsBundle.isScada(); this.description = widgetsBundle.getDescription(); this.order = widgetsBundle.getOrder(); this.externalId = widgetsBundle.getExternalId(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundleFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundleFilter.java new file mode 100644 index 0000000000..932bfa8fc8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundleFilter.java @@ -0,0 +1,38 @@ +/** + * 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. + */ +package org.thingsboard.server.common.data.widget; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@Builder +public class WidgetsBundleFilter { + + private TenantId tenantId; + private boolean fullSearch; + private boolean scadaFirst; + + public static WidgetsBundleFilter fromTenantId(TenantId tenantId) { + return WidgetsBundleFilter.builder().tenantId(tenantId).fullSearch(false).scadaFirst(false).build(); + } + + public static WidgetsBundleFilter fullSearchFromTenantId(TenantId tenantId) { + return WidgetsBundleFilter.builder().tenantId(tenantId).fullSearch(true).scadaFirst(false).build(); + } + +} diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index b0574da7b1..5646a37a68 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; +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.Tenant; @@ -865,6 +866,9 @@ public class ProtoUtils { if (isNotNull(resource.getPreview())) { builder.setPreview(ByteString.copyFrom(resource.getPreview())); } + if (isNotNull(resource.getResourceSubType())) { + builder.setResourceSubType(resource.getResourceSubType().name()); + } return builder.build(); } @@ -896,6 +900,9 @@ public class ProtoUtils { if (proto.hasPreview()) { resource.setPreview(proto.getPreview().toByteArray()); } + if (proto.hasResourceSubType()) { + resource.setResourceSubType(ResourceSubType.valueOf(proto.getResourceSubType())); + } return resource; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 800250d1cc..7c3bfddc08 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -301,6 +301,7 @@ message TbResourceProto { optional int64 externalIdLSB = 16; optional bytes data = 17; optional bytes preview = 18; + optional string resourceSubType = 19; } message ApiUsageStateProto { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index 78f76adc1d..ab52a024da 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -108,9 +108,9 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @Override public ObjectModel getObjectModel(int objectId) { LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint()); - String version = lwM2mClient.getSupportedObjectVersion(objectId).toString(); + var version = lwM2mClient.getSupportedObjectVersion(objectId); if (version != null) { - return this.getObjectModelDynamic(objectId, version); + return this.getObjectModelDynamic(objectId, version.toString()); } return null; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 43f8ce507b..27f1019a68 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -304,6 +304,7 @@ public class ModelConstants { public static final String WIDGETS_BUNDLE_ALIAS_PROPERTY = ALIAS_PROPERTY; public static final String WIDGETS_BUNDLE_TITLE_PROPERTY = TITLE_PROPERTY; public static final String WIDGETS_BUNDLE_IMAGE_PROPERTY = "image"; + public static final String WIDGETS_BUNDLE_SCADA_PROPERTY = "scada"; public static final String WIDGETS_BUNDLE_DESCRIPTION = "description"; public static final String WIDGETS_BUNDLE_ORDER = "widgets_bundle_order"; @@ -322,6 +323,8 @@ public class ModelConstants { public static final String WIDGET_TYPE_DEPRECATED_PROPERTY = "deprecated"; + public static final String WIDGET_TYPE_SCADA_PROPERTY = "scada"; + public static final String WIDGET_TYPE_WIDGET_TYPE_PROPERTY = "widget_type"; public static final String WIDGET_TYPE_INFO_VIEW_TABLE_NAME = "widget_type_info_view"; @@ -500,6 +503,7 @@ public class ModelConstants { public static final String RESOURCE_TABLE_NAME = "resource"; public static final String RESOURCE_TENANT_ID_COLUMN = TENANT_ID_COLUMN; public static final String RESOURCE_TYPE_COLUMN = "resource_type"; + public static final String RESOURCE_SUB_TYPE_COLUMN = "resource_sub_type"; public static final String RESOURCE_KEY_COLUMN = "resource_key"; public static final String RESOURCE_TITLE_COLUMN = TITLE_PROPERTY; public static final String RESOURCE_FILE_NAME_COLUMN = "file_name"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java index 6e6ee4fed8..d649d1e31a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java @@ -44,6 +44,9 @@ public abstract class AbstractWidgetTypeEntity extends @Column(name = ModelConstants.WIDGET_TYPE_DEPRECATED_PROPERTY) private boolean deprecated; + @Column(name = ModelConstants.WIDGET_TYPE_SCADA_PROPERTY) + private boolean scada; + public AbstractWidgetTypeEntity() { super(); } @@ -56,6 +59,7 @@ public abstract class AbstractWidgetTypeEntity extends this.fqn = widgetType.getFqn(); this.name = widgetType.getName(); this.deprecated = widgetType.isDeprecated(); + this.scada = widgetType.isScada(); } public AbstractWidgetTypeEntity(AbstractWidgetTypeEntity widgetTypeEntity) { @@ -64,6 +68,7 @@ public abstract class AbstractWidgetTypeEntity extends this.fqn = widgetTypeEntity.getFqn(); this.name = widgetTypeEntity.getName(); this.deprecated = widgetTypeEntity.isDeprecated(); + this.scada = widgetTypeEntity.isScada(); } protected BaseWidgetType toBaseWidgetType() { @@ -76,6 +81,7 @@ public abstract class AbstractWidgetTypeEntity extends widgetType.setFqn(fqn); widgetType.setName(name); widgetType.setDeprecated(deprecated); + widgetType.setScada(scada); return widgetType; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java index 02922c742a..0fa74b4c9f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java @@ -22,6 +22,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; +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.id.TbResourceId; @@ -40,6 +41,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN; @@ -61,6 +63,9 @@ public class TbResourceEntity extends BaseSqlEntity { @Column(name = RESOURCE_TYPE_COLUMN) private String resourceType; + @Column(name = RESOURCE_SUB_TYPE_COLUMN) + private String resourceSubType; + @Column(name = RESOURCE_KEY_COLUMN) private String resourceKey; @@ -105,6 +110,9 @@ public class TbResourceEntity extends BaseSqlEntity { } this.title = resource.getTitle(); this.resourceType = resource.getResourceType().name(); + if (resource.getResourceSubType() != null) { + this.resourceSubType = resource.getResourceSubType().name(); + } this.resourceKey = resource.getResourceKey(); this.searchText = resource.getSearchText(); this.fileName = resource.getFileName(); @@ -124,6 +132,7 @@ public class TbResourceEntity extends BaseSqlEntity { resource.setTenantId(TenantId.fromUUID(tenantId)); resource.setTitle(title); resource.setResourceType(ResourceType.valueOf(resourceType)); + resource.setResourceSubType(resourceSubType != null ? ResourceSubType.valueOf(resourceSubType) : null); resource.setResourceKey(resourceKey); resource.setSearchText(searchText); resource.setFileName(fileName); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java index 7d65d70a37..328bf38978 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java @@ -22,6 +22,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.id.TbResourceId; @@ -39,6 +40,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLU import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN; @@ -60,6 +62,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen @Column(name = RESOURCE_TYPE_COLUMN) private String resourceType; + @Column(name = RESOURCE_SUB_TYPE_COLUMN) + private String resourceSubType; + @Column(name = RESOURCE_KEY_COLUMN) private String resourceKey; @@ -96,6 +101,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen this.tenantId = resource.getTenantId().getId(); this.title = resource.getTitle(); this.resourceType = resource.getResourceType().name(); + if (resource.getResourceSubType() != null) { + this.resourceSubType = resource.getResourceSubType().name(); + } this.resourceKey = resource.getResourceKey(); this.searchText = resource.getSearchText(); this.etag = resource.getEtag(); @@ -113,6 +121,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen resource.setTenantId(TenantId.fromUUID(tenantId)); resource.setTitle(title); resource.setResourceType(ResourceType.valueOf(resourceType)); + resource.setResourceSubType(resourceSubType != null ? ResourceSubType.valueOf(resourceSubType) : null); resource.setResourceKey(resourceKey); resource.setSearchText(searchText); resource.setEtag(etag); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java index 5e2766b98d..43e2bfd457 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java @@ -47,6 +47,9 @@ public final class WidgetsBundleEntity extends BaseVersionedEntity getImagesByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink) { log.trace("Executing getImagesByTenantId [{}]", tenantId); TbResourceInfoFilter filter = TbResourceInfoFilter.builder() .tenantId(tenantId) .resourceTypes(Set.of(ResourceType.IMAGE)) + .resourceSubTypes(Set.of(imageSubType)) .build(); return findTenantResourcesByTenantId(filter, pageLink); } @Override - public PageData getAllImagesByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData getAllImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink) { log.trace("Executing getAllImagesByTenantId [{}]", tenantId); TbResourceInfoFilter filter = TbResourceInfoFilter.builder() .tenantId(tenantId) .resourceTypes(Set.of(ResourceType.IMAGE)) + .resourceSubTypes(Set.of(imageSubType)) .build(); return findAllTenantResourcesByTenantId(filter, pageLink); } @@ -280,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); @@ -419,7 +431,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic return base64ToImageUrl(tenantId, name, data, false); } - private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:(.*):(.*);data:(.*);.*"); + private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:([^:]*):([^:]*):?([^:]*)?;data:(.*);.*"); private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) { if (StringUtils.isBlank(data)) { @@ -429,11 +441,15 @@ public class BaseImageService extends BaseResourceService implements ImageServic boolean matches = matcher.matches(); String mdResourceKey = null; String mdResourceName = null; + String mdResourceSubType = null; String mdMediaType; if (matches) { mdResourceKey = new String(Base64.getDecoder().decode(matcher.group(1)), StandardCharsets.UTF_8); mdResourceName = new String(Base64.getDecoder().decode(matcher.group(2)), StandardCharsets.UTF_8); - mdMediaType = matcher.group(3); + if (StringUtils.isNotBlank(matcher.group(3))) { + mdResourceSubType = new String(Base64.getDecoder().decode(matcher.group(3)), StandardCharsets.UTF_8); + }; + mdMediaType = matcher.group(4); } else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) { mdMediaType = StringUtils.substringBetween(data, "data:", ";base64"); } else { @@ -462,6 +478,11 @@ public class BaseImageService extends BaseResourceService implements ImageServic } else { fileName = mdResourceKey; } + if (StringUtils.isBlank(mdResourceSubType)) { + image.setResourceSubType(ResourceSubType.IMAGE); + } else { + image.setResourceSubType(ResourceSubType.valueOf(mdResourceSubType)); + } image.setFileName(fileName); image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mdMediaType)); image.setData(imageData); @@ -623,7 +644,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic String tbImagePrefix = ""; if (addTbImagePrefix) { tbImagePrefix = "tb-image:" + Base64.getEncoder().encodeToString(imageInfo.getResourceKey().getBytes(StandardCharsets.UTF_8)) + ":" - + Base64.getEncoder().encodeToString(imageInfo.getName().getBytes(StandardCharsets.UTF_8)) + ";"; + + Base64.getEncoder().encodeToString(imageInfo.getName().getBytes(StandardCharsets.UTF_8)) + ":" + + Base64.getEncoder().encodeToString(imageInfo.getResourceSubType().name().getBytes(StandardCharsets.UTF_8)) + ";"; } return tbImagePrefix + "data:" + descriptor.getMediaType() + ";base64," + Base64.getEncoder().encodeToString(data); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index bc641bb1fc..fcace55c93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -187,7 +187,7 @@ public class BaseResourceService extends AbstractCachedEntityService findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType resourceType, String[] objectIds) { log.trace("Executing findTenantResourcesByResourceTypeAndObjectIds [{}][{}][{}]", tenantId, resourceType, objectIds); validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, objectIds, null); + return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, null, objectIds, null); } @Override @@ -201,7 +201,7 @@ public class BaseResourceService extends AbstractCachedEntityService findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType resourceType, PageLink pageLink) { log.trace("Executing findTenantResourcesByResourceTypeAndPageLink [{}][{}][{}]", tenantId, resourceType, pageLink); validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, pageLink); + return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, null, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java index b776b49cc9..c236a4a5d5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.resource; +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.id.TbResourceId; @@ -35,10 +36,12 @@ public interface TbResourceDao extends Dao, TenantEntityWithDataDao, PageData findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + ResourceSubType resourceSubType, PageLink pageLink); List findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + ResourceSubType resourceSubType, String[] objectIds, String searchText); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java index cb2ab5aa42..f8927aaa81 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +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.id.TbResourceId; @@ -68,11 +69,13 @@ public class JpaTbResourceDao extends JpaAbstractDao findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + ResourceSubType resourceSubType, PageLink pageLink) { return DaoUtil.toPageData(resourceRepository.findResourcesPage( tenantId.getId(), TenantId.SYS_TENANT_ID.getId(), resourceType.name(), + resourceSubType != null ? resourceSubType.name() : null, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink) )); @@ -80,6 +83,7 @@ public class JpaTbResourceDao extends JpaAbstractDao findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + ResourceSubType resourceSubType, String[] objectIds, String searchText) { return objectIds == null ? @@ -87,6 +91,7 @@ public class JpaTbResourceDao extends JpaAbstractDao resourceSubTypes = filter.getResourceSubTypes(); return DaoUtil.toPageData(resourceInfoRepository .findAllTenantResourcesByTenantId( filter.getTenantId().getId(), TenantId.NULL_UUID, resourceTypes.stream().map(Enum::name).collect(Collectors.toList()), + CollectionsUtil.isEmpty(resourceSubTypes) ? null : + resourceSubTypes.stream().map(Enum::name).collect(Collectors.toList()), Objects.toString(pageLink.getTextSearch(), ""), DaoUtil.toPageable(pageLink))); } @@ -77,10 +81,13 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao resourceSubTypes = filter.getResourceSubTypes(); return DaoUtil.toPageData(resourceInfoRepository .findTenantResourcesByTenantId( filter.getTenantId().getId(), resourceTypes.stream().map(Enum::name).collect(Collectors.toList()), + CollectionsUtil.isEmpty(resourceSubTypes) ? null : + resourceSubTypes.stream().map(Enum::name).collect(Collectors.toList()), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index c092ea3292..00c51b50a2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -37,19 +37,23 @@ public interface TbResourceInfoRepository extends JpaRepository findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, @Param("systemTenantId") UUID systemTenantId, @Param("resourceTypes") List resourceTypes, + @Param("resourceSubTypes") List resourceSubTypes, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " + "ri.tenantId = :tenantId " + "AND ri.resourceType IN :resourceTypes " + + "AND (:resourceSubTypes IS NULL OR ri.resourceSubType IN :resourceSubTypes) " + "AND (:searchText IS NULL OR ilike(ri.title, CONCAT('%', :searchText, '%')) = true)") Page findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, @Param("resourceTypes") List resourceTypes, + @Param("resourceSubTypes") List resourceSubTypes, @Param("searchText") String searchText, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java index f5a831ff64..ce2e6b890b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -34,6 +34,7 @@ public interface TbResourceRepository extends JpaRepository findResources(@Param("tenantId") UUID tenantId, @Param("systemAdminId") UUID sysAdminId, @Param("resourceType") String resourceType, + @Param("resourceSubType") String resourceSubType, @Param("searchText") String searchText); @Query("SELECT tr FROM TbResourceEntity tr " + 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..6fb4bcb85d 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 @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.DeprecatedFilter; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.DaoUtil; @@ -84,57 +85,60 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { - boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter); - boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter); - boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty(); + public PageData findSystemWidgetTypes(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) { + boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(widgetTypeFilter.getDeprecatedFilter()); + boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(widgetTypeFilter.getDeprecatedFilter()); + boolean widgetTypesEmpty = widgetTypeFilter.getWidgetTypes() == null || widgetTypeFilter.getWidgetTypes().isEmpty(); return DaoUtil.toPageData( widgetTypeInfoRepository .findSystemWidgetTypes( NULL_UUID, pageLink.getTextSearch(), - fullSearch, + widgetTypeFilter.isFullSearch(), deprecatedFilterEnabled, deprecatedFilterBool, widgetTypesEmpty, - widgetTypes == null ? Collections.emptyList() : widgetTypes, + widgetTypeFilter.getWidgetTypes() == null ? Collections.emptyList() : widgetTypeFilter.getWidgetTypes(), + widgetTypeFilter.isScadaFirst(), DaoUtil.toPageable(pageLink, WidgetTypeInfoEntity.SEARCH_COLUMNS_MAP))); } @Override - public PageData findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { - boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter); - boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter); - boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty(); + public PageData findAllTenantWidgetTypesByTenantId(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) { + boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(widgetTypeFilter.getDeprecatedFilter()); + boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(widgetTypeFilter.getDeprecatedFilter()); + boolean widgetTypesEmpty = widgetTypeFilter.getWidgetTypes() == null || widgetTypeFilter.getWidgetTypes().isEmpty(); return DaoUtil.toPageData( widgetTypeInfoRepository .findAllTenantWidgetTypesByTenantId( - tenantId, + widgetTypeFilter.getTenantId().getId(), NULL_UUID, pageLink.getTextSearch(), - fullSearch, + widgetTypeFilter.isFullSearch(), deprecatedFilterEnabled, deprecatedFilterBool, widgetTypesEmpty, - widgetTypes == null ? Collections.emptyList() : widgetTypes, + widgetTypeFilter.getWidgetTypes() == null ? Collections.emptyList() : widgetTypeFilter.getWidgetTypes(), + widgetTypeFilter.isScadaFirst(), DaoUtil.toPageable(pageLink, WidgetTypeInfoEntity.SEARCH_COLUMNS_MAP))); } @Override - public PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { - boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter); - boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter); - boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty(); + public PageData findTenantWidgetTypesByTenantId(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) { + boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(widgetTypeFilter.getDeprecatedFilter()); + boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(widgetTypeFilter.getDeprecatedFilter()); + boolean widgetTypesEmpty = widgetTypeFilter.getWidgetTypes() == null || widgetTypeFilter.getWidgetTypes().isEmpty(); return DaoUtil.toPageData( widgetTypeInfoRepository .findTenantWidgetTypesByTenantId( - tenantId, + widgetTypeFilter.getTenantId().getId(), pageLink.getTextSearch(), - fullSearch, + widgetTypeFilter.isFullSearch(), deprecatedFilterEnabled, deprecatedFilterBool, widgetTypesEmpty, - widgetTypes == null ? Collections.emptyList() : widgetTypes, + widgetTypeFilter.getWidgetTypes() == null ? Collections.emptyList() : widgetTypeFilter.getWidgetTypes(), + widgetTypeFilter.isScadaFirst(), DaoUtil.toPageable(pageLink, WidgetTypeInfoEntity.SEARCH_COLUMNS_MAP))); } @@ -176,6 +180,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/JpaWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java index 668eb0bcbf..290e949c38 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -64,8 +65,8 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findSystemWidgetsBundles(TenantId tenantId, boolean fullSearch, PageLink pageLink) { - if (fullSearch) { + public PageData findSystemWidgetsBundles(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { + if (widgetsBundleFilter.isFullSearch()) { return DaoUtil.toPageData( widgetsBundleRepository .findSystemWidgetsBundlesFullSearch( @@ -93,13 +94,13 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findAllTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) { - return findTenantWidgetsBundlesByTenantIds(Arrays.asList(tenantId, NULL_UUID), fullSearch, pageLink); + public PageData findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { + return findTenantWidgetsBundlesByTenantIds(Arrays.asList(widgetsBundleFilter.getTenantId().getId(), NULL_UUID), widgetsBundleFilter, pageLink); } @Override - public PageData findTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) { - return findTenantWidgetsBundlesByTenantIds(Collections.singletonList(tenantId), fullSearch, pageLink); + public PageData findTenantWidgetsBundlesByTenantId(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { + return findTenantWidgetsBundlesByTenantIds(Collections.singletonList(widgetsBundleFilter.getTenantId().getId()), widgetsBundleFilter, pageLink); } @Override @@ -107,13 +108,14 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findTenantWidgetsBundlesByTenantIds(List tenantIds, boolean fullSearch, PageLink pageLink) { - if (fullSearch) { + private PageData findTenantWidgetsBundlesByTenantIds(List tenantIds, WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { + if (widgetsBundleFilter.isFullSearch()) { return DaoUtil.toPageData( widgetsBundleRepository .findAllTenantWidgetsBundlesByTenantIdsFullSearch( tenantIds, pageLink.getTextSearch(), + widgetsBundleFilter.isScadaFirst(), DaoUtil.toPageable(pageLink))); } else { return DaoUtil.toPageData( diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java index 85079bb7a8..4b85fcb7dc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java @@ -41,7 +41,8 @@ public interface WidgetTypeInfoRepository extends JpaRepository widgetTypes, + @Param("scadaFirst") boolean scadaFirst, Pageable pageable); @Query(nativeQuery = true, @@ -80,7 +82,8 @@ public interface WidgetTypeInfoRepository extends JpaRepository widgetTypes, + @Param("scadaFirst") boolean scadaFirst, Pageable pageable); @Query(nativeQuery = true, @@ -120,7 +124,8 @@ public interface WidgetTypeInfoRepository extends JpaRepository widgetTypes, + @Param("scadaFirst") boolean scadaFirst, Pageable pageable); @Query("SELECT wti FROM WidgetTypeInfoEntity wti, WidgetsBundleWidgetEntity wbw " + 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/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java index 26e13b1650..8de78986ae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java @@ -106,7 +106,7 @@ public interface WidgetsBundleRepository extends JpaRepository findAllTenantWidgetsBundlesByTenantIdsFullSearch(@Param("tenantIds") List tenantIds, - @Param("textSearch") String textSearch, - Pageable pageable); + @Param("textSearch") String textSearch, + @Param("scadaFirst") boolean scadaFirst, + Pageable pageable); WidgetsBundleEntity findFirstByTenantIdAndTitle(UUID tenantId, String title); 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..e92adba01d 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,45 @@ public class ImageUtils { private ProcessedImage preview; } + @Data + public static class ScadaSymbolMetadataInfo { + private String title; + private String description; + private String[] searchTags; + private int widgetSizeX; + private int widgetSizeY; + + 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]; + } + if (metaData != null && metaData.has("widgetSizeX")) { + widgetSizeX = metaData.get("widgetSizeX").asInt(); + } else { + widgetSizeX = 3; + } + if (metaData != null && metaData.has("widgetSizeY")) { + widgetSizeY = metaData.get("widgetSizeY").asInt(); + } else { + widgetSizeY = 3; + } + } + } + } 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..d90c105aae 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.DeprecatedFilter; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.Dao; @@ -55,11 +56,11 @@ public interface WidgetTypeDao extends Dao, ExportableEntityD boolean existsByTenantIdAndId(TenantId tenantId, UUID widgetTypeId); - PageData findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); + PageData findSystemWidgetTypes(WidgetTypeFilter widgetTypeFilter, PageLink pageLink); - PageData findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); + PageData findAllTenantWidgetTypesByTenantId(WidgetTypeFilter widgetTypeFilter, PageLink pageLink); - PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); + PageData findTenantWidgetTypesByTenantId(WidgetTypeFilter widgetTypeFilter, PageLink pageLink); /** * Find widget types by widgetsBundleId. @@ -92,6 +93,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..3e2fe0bf35 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 @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.DeprecatedFilter; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; @@ -121,28 +122,30 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { } @Override - public PageData findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { - log.trace("Executing findSystemWidgetTypesByPageLink, fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]", fullSearch, deprecatedFilter, widgetTypes, pageLink); + public PageData findSystemWidgetTypesByPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) { + log.trace("Executing findSystemWidgetTypesByPageLink, pageLink [{}]", pageLink); Validator.validatePageLink(pageLink); - return widgetTypeDao.findSystemWidgetTypes(tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink); + return widgetTypeDao.findSystemWidgetTypes(widgetTypeFilter, pageLink); } @Override - public PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { - log.trace("Executing findAllTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]", - tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink); + public PageData findAllTenantWidgetTypesByTenantIdAndPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) { + TenantId tenantId = widgetTypeFilter.getTenantId(); + log.trace("Executing findAllTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", + tenantId, pageLink); Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); - return widgetTypeDao.findAllTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink); + return widgetTypeDao.findAllTenantWidgetTypesByTenantId(widgetTypeFilter, pageLink); } @Override - public PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { - log.trace("Executing findTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]", - tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink); + public PageData findTenantWidgetTypesByTenantIdAndPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) { + TenantId tenantId = widgetTypeFilter.getTenantId(); + log.trace("Executing findTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", + tenantId, pageLink); Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); - return widgetTypeDao.findTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink); + return widgetTypeDao.findTenantWidgetTypesByTenantId(widgetTypeFilter, pageLink); } @Override @@ -189,6 +192,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); @@ -251,7 +263,13 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { - return widgetTypeDao.findTenantWidgetTypesByTenantId(id.getId(), false, DeprecatedFilter.ALL, null, pageLink); + return widgetTypeDao.findTenantWidgetTypesByTenantId( + WidgetTypeFilter.builder() + .tenantId(id) + .fullSearch(false) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), + pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java index b54cd66499..a37149ae88 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ImageContainerDao; @@ -55,7 +56,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa * @param pageLink the page link * @return the list of widgets bundles objects */ - PageData findSystemWidgetsBundles(TenantId tenantId, boolean fullSearch, PageLink pageLink); + PageData findSystemWidgetsBundles(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink); /** * Find tenant widgets bundles by tenantId and page link. @@ -73,7 +74,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa * @param pageLink the page link * @return the list of widgets bundles objects */ - PageData findAllTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink); + PageData findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink); /** * Find all tenant widgets bundles (does not include system) by tenantId and page link. @@ -82,7 +83,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa * @param pageLink the page link * @return the list of widgets bundles objects */ - PageData findTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink); + PageData findTenantWidgetsBundlesByTenantId(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink); PageData findAllWidgetsBundles(PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index ae53c70ec8..d7006e9e56 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; @@ -118,10 +119,10 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { } @Override - public PageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { - log.trace("Executing findSystemWidgetsBundles, fullSearch [{}], pageLink [{}]", fullSearch, pageLink); + public PageData findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { + log.trace("Executing findSystemWidgetsBundles, widgetsBundleFilter [{}], pageLink [{}]", widgetsBundleFilter, pageLink); Validator.validatePageLink(pageLink); - return widgetsBundleDao.findSystemWidgetsBundles(tenantId, fullSearch, pageLink); + return widgetsBundleDao.findSystemWidgetsBundles(widgetsBundleFilter, pageLink); } @Override @@ -131,7 +132,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { PageLink pageLink = new PageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT); PageData pageData; do { - pageData = findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink); + pageData = findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink); widgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -149,19 +150,21 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { } @Override - public PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { - log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink); + public PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { + TenantId tenantId = widgetsBundleFilter.getTenantId(); + log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink); Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); - return widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId.getId(), fullSearch, pageLink); + return widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(widgetsBundleFilter, pageLink); } @Override - public PageData findTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { - log.trace("Executing findTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink); + public PageData findTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { + TenantId tenantId = widgetsBundleFilter.getTenantId(); + log.trace("Executing findTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink); Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); - return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId.getId(), fullSearch, pageLink); + return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(widgetsBundleFilter, pageLink); } @Override @@ -172,7 +175,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { PageLink pageLink = new PageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT); PageData pageData; do { - pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink); + pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink); widgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9cd734d80b..66fe8dc1bb 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -506,6 +506,7 @@ CREATE TABLE IF NOT EXISTS widget_type ( name varchar(255), tenant_id uuid, image varchar(1000000), + scada boolean NOT NULL DEFAULT false, deprecated boolean NOT NULL DEFAULT false, description varchar(1024), tags text[], @@ -522,6 +523,7 @@ CREATE TABLE IF NOT EXISTS widgets_bundle ( tenant_id uuid, title varchar(255), image varchar(1000000), + scada boolean NOT NULL DEFAULT false, description varchar(1024), widgets_bundle_order int, external_id uuid, @@ -737,6 +739,7 @@ CREATE TABLE IF NOT EXISTS resource ( tenant_id uuid NOT NULL, title varchar(255) NOT NULL, resource_type varchar(32) NOT NULL, + resource_sub_type varchar(32), resource_key varchar(255) NOT NULL, search_text varchar(255), file_name varchar(255) NOT NULL, diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java index 314a361dd2..d4c00fdf39 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -174,7 +175,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest { PageLink pageLink = new PageLink(19); PageData pageData = null; do { - pageData = widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink); + pageData = widgetsBundleService.findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -297,7 +298,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest { PageLink pageLink = new PageLink(17); PageData pageData = null; do { - pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink); + pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -314,7 +315,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest { loadedWidgetsBundles.clear(); pageLink = new PageLink(14); do { - pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink); + pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -336,7 +337,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest { loadedWidgetsBundles.clear(); pageLink = new PageLink(18); do { - pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink); + pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink); loadedWidgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java index e8f3bba9db..8010eab4cd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.widget.BaseWidgetType; import org.thingsboard.server.common.data.widget.DeprecatedFilter; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; @@ -151,12 +152,22 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { @Test public void testFindSystemWidgetTypes() { - PageData widgetTypes = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.singletonList("static"), + PageData widgetTypes = widgetTypeDao.findSystemWidgetTypes( + WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(Collections.singletonList("static")).build(), new PageLink(1024, 0, "TYPE_DESCRIPTION", new SortOrder("createdTime"))); assertEquals(1, widgetTypes.getData().size()); assertEquals(new WidgetTypeInfo(widgetTypeList.get(1)), widgetTypes.getData().get(0)); - widgetTypes = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.emptyList(), + widgetTypes = widgetTypeDao.findSystemWidgetTypes( + WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(Collections.emptyList()).build(), new PageLink(1024, 0, "hfgfd tag2_2 ghg", new SortOrder("createdTime"))); assertEquals(1, widgetTypes.getData().size()); assertEquals(new WidgetTypeInfo(widgetTypeList.get(2)), widgetTypes.getData().get(0)); @@ -175,12 +186,21 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { sameNameList.sort(Comparator.comparing(BaseWidgetType::getName).thenComparing((BaseWidgetType baseWidgetType) -> baseWidgetType.getId().getId())); List expected = sameNameList.stream().map(WidgetTypeInfo::new).collect(Collectors.toList()); - PageData widgetTypesFirstPage = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.singletonList("static"), + PageData widgetTypesFirstPage = widgetTypeDao.findSystemWidgetTypes( + WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(Collections.singletonList("static")).build(), new PageLink(10, 0, null, new SortOrder("name"))); assertEquals(10, widgetTypesFirstPage.getData().size()); assertThat(widgetTypesFirstPage.getData()).containsExactlyElementsOf(expected.subList(0, 10)); - PageData widgetTypesSecondPage = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.singletonList("static"), + PageData widgetTypesSecondPage = widgetTypeDao.findSystemWidgetTypes(WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(Collections.singletonList("static")).build(), new PageLink(10, 1, null, new SortOrder("name"))); assertEquals(10, widgetTypesSecondPage.getData().size()); assertThat(widgetTypesSecondPage.getData()).containsExactlyElementsOf(expected.subList(10, 20)); @@ -195,7 +215,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags); PageData widgetTypes = widgetTypeDao.findSystemWidgetTypes( - TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText) + WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), + new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(1); @@ -211,7 +236,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags); PageData widgetTypes = widgetTypeDao.findSystemWidgetTypes( - TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText) + WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), + new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(0); @@ -227,7 +257,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { var widgetType = createAndSaveWidgetType(new TenantId(tenantId), i); widgetTypeList.add(widgetType); } - PageData widgetTypes = widgetTypeDao.findTenantWidgetTypesByTenantId(tenantId, true, DeprecatedFilter.ALL, null, + PageData widgetTypes = widgetTypeDao.findTenantWidgetTypesByTenantId( + WidgetTypeFilter.builder() + .tenantId(new TenantId(tenantId)) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), new PageLink(10, 0, "", new SortOrder("createdTime"))); assertEquals(WIDGET_TYPE_COUNT, widgetTypes.getData().size()); assertEquals(new WidgetTypeInfo(widgetTypeList.get(3)), widgetTypes.getData().get(0)); @@ -244,7 +279,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags); PageData widgetTypes = widgetTypeDao.findTenantWidgetTypesByTenantId( - TenantId.SYS_TENANT_ID.getId(), true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText) + WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), + new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(1); @@ -260,7 +300,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags); PageData widgetTypes = widgetTypeDao.findTenantWidgetTypesByTenantId( - TenantId.SYS_TENANT_ID.getId(), true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText) + WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), + new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(0); @@ -278,7 +323,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags); PageData widgetTypes = widgetTypeDao.findAllTenantWidgetTypesByTenantId( - TenantId.SYS_TENANT_ID.getId(), true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText) + WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), + new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(1); @@ -294,7 +344,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags); PageData widgetTypes = widgetTypeDao.findAllTenantWidgetTypesByTenantId( - TenantId.SYS_TENANT_ID.getId(), true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText) + WidgetTypeFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .fullSearch(true) + .deprecatedFilter(DeprecatedFilter.ALL) + .widgetTypes(null).build(), + new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(0); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java index e2221dcc93..5ec50189c4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.widget.WidgetTypeDao; @@ -42,6 +43,8 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { @@ -113,11 +116,11 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { assertEquals(30, widgetsBundles.size()); // Get first page PageLink pageLink = new PageLink(10, 0, "WB"); - PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, false, pageLink); + PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fromTenantId(TenantId.SYS_TENANT_ID), pageLink); assertEquals(10, widgetsBundles1.getData().size()); // Get next page pageLink = pageLink.nextPageLink(); - PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, false, pageLink); + PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fromTenantId(TenantId.SYS_TENANT_ID), pageLink); assertEquals(10, widgetsBundles2.getData().size()); } @@ -142,19 +145,19 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle3.getId(), widgetType2.getId(), 1)); PageLink pageLink = new PageLink(10, 0, "widget type 1", new SortOrder("title")); - PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, true, pageLink); + PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), pageLink); assertEquals(2, widgetsBundles1.getData().size()); assertEquals(widgetsBundle1, widgetsBundles1.getData().get(0)); assertEquals(widgetsBundle3, widgetsBundles1.getData().get(1)); pageLink = new PageLink(10, 0, "Test widget type 2", new SortOrder("title")); - PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, true, pageLink); + PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), pageLink); assertEquals(2, widgetsBundles2.getData().size()); assertEquals(widgetsBundle2, widgetsBundles2.getData().get(0)); assertEquals(widgetsBundle3, widgetsBundles2.getData().get(1)); pageLink = new PageLink(10, 0, "ppp Fd v TAG1 tt", new SortOrder("title")); - PageData widgetsBundles3 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, true, pageLink); + PageData widgetsBundles3 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), pageLink); assertEquals(2, widgetsBundles3.getData().size()); assertEquals(widgetsBundle1, widgetsBundles3.getData().get(0)); assertEquals(widgetsBundle3, widgetsBundles3.getData().get(1)); @@ -171,7 +174,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(systemWidgetBundle.getId(), widgetType.getId(), 0)); PageData widgetTypes = widgetsBundleDao.findSystemWidgetsBundles( - TenantId.SYS_TENANT_ID, true, new PageLink(10, 0, searchText) + WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(1); @@ -191,7 +194,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(systemWidgetBundle.getId(), widgetType.getId(), 0)); PageData widgetTypes = widgetsBundleDao.findSystemWidgetsBundles( - TenantId.SYS_TENANT_ID, true, new PageLink(10, 0, searchText) + WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(0); @@ -245,19 +248,19 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { assertEquals(100, widgetsBundleDao.find(TenantId.SYS_TENANT_ID).size()); PageLink pageLink = new PageLink(30, 0, "WB"); - PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink); + PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId1)), pageLink); assertEquals(30, widgetsBundles1.getData().size()); pageLink = pageLink.nextPageLink(); - PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink); + PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId1)), pageLink); assertEquals(30, widgetsBundles2.getData().size()); pageLink = pageLink.nextPageLink(); - PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink); + PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId1)), pageLink); assertEquals(10, widgetsBundles3.getData().size()); pageLink = pageLink.nextPageLink(); - PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink); + PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId1)), pageLink); assertEquals(0, widgetsBundles4.getData().size()); } @@ -287,29 +290,75 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle3.getId(), widgetType2.getId(), 1)); PageLink pageLink = new PageLink(10, 0, "widget type 1", new SortOrder("title")); - PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink); + PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId1)), pageLink); assertEquals(1, widgetsBundles1.getData().size()); assertEquals(widgetsBundle1, widgetsBundles1.getData().get(0)); pageLink = new PageLink(10, 0, "Test widget type 2", new SortOrder("title")); - PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink); + PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId1)), pageLink); assertEquals(0, widgetsBundles2.getData().size()); - PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId2, true, pageLink); + PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId2)), pageLink); assertEquals(2, widgetsBundles3.getData().size()); assertEquals(widgetsBundle2, widgetsBundles3.getData().get(0)); assertEquals(widgetsBundle3, widgetsBundles3.getData().get(1)); pageLink = new PageLink(10, 0, "ttt Tag2 ffff hhhh", new SortOrder("title")); - PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink); + PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId1)), pageLink); assertEquals(1, widgetsBundles4.getData().size()); assertEquals(widgetsBundle1, widgetsBundles4.getData().get(0)); - PageData widgetsBundles5 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId2, true, pageLink); + PageData widgetsBundles5 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId2)), pageLink); assertEquals(1, widgetsBundles5.getData().size()); assertEquals(widgetsBundle3, widgetsBundles5.getData().get(0)); } + @Test + public void testFindAllWidgetsBundlesByTenantIdFullSearchScadaFirst() { + UUID tenantId1 = Uuids.timeBased(); + UUID tenantId2 = Uuids.timeBased(); + for (int i = 0; i < 10; i++) { + createWidgetBundles(5, tenantId1, "WB1_" + i + "_"); + createWidgetBundles(2, tenantId1, "WB1_SCADA_" + i + "_", true); + createWidgetBundles(3, tenantId2, "WB2_" + i + "_"); + createWidgetBundles(3, tenantId2, "WB2_SCADA_" + i + "_", true); + createSystemWidgetBundles(2, "WB_SYS_" + i + "_"); + createSystemWidgetBundles(1, "WB_SYS_SCADA_" + i + "_", true); + } + widgetsBundles = widgetsBundleDao.find(TenantId.SYS_TENANT_ID).stream().sorted(Comparator.comparing(WidgetsBundle::getTitle)).collect(Collectors.toList());; + assertEquals(160, widgetsBundles.size()); + + PageLink pageLink = new PageLink(50, 0, "WB", new SortOrder("title")); + PageData widgetsBundles1 = + widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId( + WidgetsBundleFilter.builder().tenantId(TenantId.fromUUID(tenantId1)).fullSearch(true).scadaFirst(true).build(), pageLink); + + for (int i =0; i < 30; i++) { + var widgetsBundle = widgetsBundles1.getData().get(i); + assertTrue(widgetsBundle.isScada()); + } + + for (int i = 30; i < 50; i++) { + var widgetsBundle = widgetsBundles1.getData().get(i); + assertFalse(widgetsBundle.isScada()); + } + + pageLink = new PageLink(50, 0, "WB", new SortOrder("title")); + PageData widgetsBundles2 = + widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId( + WidgetsBundleFilter.builder().tenantId(TenantId.fromUUID(tenantId2)).fullSearch(true).scadaFirst(true).build(), pageLink); + + for (int i =0; i < 40; i++) { + var widgetsBundle = widgetsBundles2.getData().get(i); + assertTrue(widgetsBundle.isScada()); + } + + for (int i = 40; i < 50; i++) { + var widgetsBundle = widgetsBundles2.getData().get(i); + assertFalse(widgetsBundle.isScada()); + } + } + @Test public void testTagsSearchInFindAllWidgetsBundlesByTenantId() { for (var entry : SHOULD_FIND_SEARCH_TO_TAGS_MAP.entrySet()) { @@ -321,7 +370,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(systemWidgetBundle.getId(), widgetType.getId(), 0)); PageData widgetTypes = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId( - TenantId.SYS_TENANT_ID.getId(), true, new PageLink(10, 0, searchText) + WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(1); @@ -341,7 +390,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(systemWidgetBundle.getId(), widgetType.getId(), 0)); PageData widgetTypes = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId( - TenantId.SYS_TENANT_ID.getId(), true, new PageLink(10, 0, searchText) + WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), new PageLink(10, 0, searchText) ); assertThat(widgetTypes.getData()).hasSize(0); @@ -378,7 +427,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { }).collect(Collectors.toList());; assertEquals(20, widgetsBundles.size()); PageLink pageLink = new PageLink(100, 0, "", new SortOrder("title")); - PageData widgetsBundlesData = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink); + PageData widgetsBundlesData = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId1)), pageLink); assertEquals(20, widgetsBundlesData.getData().size()); assertEquals(widgetsBundles, widgetsBundlesData.getData()); } @@ -391,19 +440,27 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { widgetsBundles = widgetsBundleDao.find(TenantId.SYS_TENANT_ID); assertEquals(10, widgetsBundleDao.find(TenantId.SYS_TENANT_ID).size()); PageLink textPageLink = new PageLink(30, 0, "TEXT_NOT_FOUND"); - PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId, false, textPageLink); + PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId)), textPageLink); assertEquals(0, widgetsBundles4.getData().size()); } private void createWidgetBundles(int count, UUID tenantId, String prefix) { + createWidgetBundles(count, tenantId, prefix, false); + } + + private void createWidgetBundles(int count, UUID tenantId, String prefix, boolean scada) { for (int i = 0; i < count; i++) { - createWidgetsBundle(TenantId.fromUUID(tenantId), prefix + i, prefix + i, null); + createWidgetsBundle(TenantId.fromUUID(tenantId), prefix + i, prefix + i, null, scada); } } private void createSystemWidgetBundles(int count, String prefix) { + createSystemWidgetBundles(count, prefix, false); + } + + private void createSystemWidgetBundles(int count, String prefix, boolean scada) { for (int i = 0; i < count; i++) { - createWidgetsBundle(TenantId.SYS_TENANT_ID, prefix + i, prefix + i, null); + createWidgetsBundle(TenantId.SYS_TENANT_ID, prefix + i, prefix + i, null, scada); } } @@ -412,12 +469,17 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { } private WidgetsBundle createWidgetsBundle(TenantId tenantId, String alias, String title, Integer order) { + return createWidgetsBundle(tenantId, alias, title, order, false); + } + + private WidgetsBundle createWidgetsBundle(TenantId tenantId, String alias, String title, Integer order, boolean scada) { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setAlias(alias); widgetsBundle.setTitle(title); widgetsBundle.setTenantId(tenantId); widgetsBundle.setId(new WidgetsBundleId(Uuids.timeBased())); widgetsBundle.setOrder(order); + widgetsBundle.setScada(scada); return widgetsBundleDao.save(TenantId.SYS_TENANT_ID, widgetsBundle); } diff --git a/dao/src/test/resources/sql-test.properties b/dao/src/test/resources/sql-test.properties index 4292db28d6..eb50e96e75 100644 --- a/dao/src/test/resources/sql-test.properties +++ b/dao/src/test/resources/sql-test.properties @@ -14,7 +14,7 @@ spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=none spring.datasource.username=postgres spring.datasource.password=postgres -spring.datasource.url=jdbc:tc:postgresql:12.8:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb +spring.datasource.url=jdbc:tc:postgresql:12.19:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.hikari.maximumPoolSize=16 diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 6bc4d82c5b..0aa51402c5 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -58,6 +58,7 @@ import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.ImageExportData; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.SystemInfo; @@ -3627,10 +3628,19 @@ public class RestClient implements Closeable { } public PageData getImages(PageLink pageLink, boolean includeSystemImages) { + return this.getImages(pageLink, null, includeSystemImages); + } + + public PageData getImages(PageLink pageLink, ResourceSubType imageSubType, boolean includeSystemImages) { Map params = new HashMap<>(); + var url = baseURL + "/api/images?includeSystemImages={includeSystemImages}&"; addPageLinkToParam(params, pageLink); params.put("includeSystemImages", String.valueOf(includeSystemImages)); - return restTemplate.exchange(baseURL + "/api/images?includeSystemImages={includeSystemImages}&" + getUrlParams(pageLink), + if (imageSubType != null) { + url += "imageSubType={imageSubType}&"; + params.put("imageSubType", imageSubType.name()); + } + return restTemplate.exchange(url + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() {}, diff --git a/ui-ngx/package.json b/ui-ngx/package.json index f383054ece..916b72cc15 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -45,6 +45,7 @@ "@ngx-translate/core": "^14.0.0", "@svgdotjs/svg.filter.js": "^3.0.8", "@svgdotjs/svg.js": "^3.2.0", + "@svgdotjs/svg.panzoom.js": "^2.1.2", "@tinymce/tinymce-angular": "^7.0.0", "ace-builds": "1.4.13", "ace-diff": "^3.0.3", diff --git a/ui-ngx/patches/angular-gridster2+15.0.4.patch b/ui-ngx/patches/angular-gridster2+15.0.4.patch new file mode 100644 index 0000000000..a6642928c3 --- /dev/null +++ b/ui-ngx/patches/angular-gridster2+15.0.4.patch @@ -0,0 +1,44 @@ +diff --git a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs +index cf4e220..4275d11 100644 +--- a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs ++++ b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs +@@ -208,6 +208,7 @@ const GridsterConfigService = { + useTransformPositioning: true, + scrollSensitivity: 10, + scrollSpeed: 20, ++ colWidthUpdateCallback: undefined, + initCallback: undefined, + destroyCallback: undefined, + gridSizeChangedCallback: undefined, +@@ -1243,6 +1244,9 @@ class GridsterComponent { + this.renderer.setStyle(this.el, 'padding-right', this.$options.margin + 'px'); + } + this.curColWidth = (this.curWidth - marginWidth) / this.columns; ++ if (this.options.colWidthUpdateCallback) { ++ this.curColWidth = this.options.colWidthUpdateCallback(this.curColWidth); ++ } + let marginHeight = -this.$options.margin; + if (this.$options.outerMarginTop !== null) { + marginHeight += this.$options.outerMarginTop; +@@ -1266,6 +1270,9 @@ class GridsterComponent { + } + else { + this.curColWidth = (this.curWidth + this.$options.margin) / this.columns; ++ if (this.options.colWidthUpdateCallback) { ++ this.curColWidth = this.options.colWidthUpdateCallback(this.curColWidth); ++ } + this.curRowHeight = + ((this.curHeight + this.$options.margin) / this.rows) * + this.$options.rowHeightRatio; +diff --git a/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts b/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts +index 1d7cdf0..a712b35 100644 +--- a/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts ++++ b/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts +@@ -73,6 +73,7 @@ export interface GridsterConfig { + useTransformPositioning?: boolean; + scrollSensitivity?: number | null; + scrollSpeed?: number; ++ colWidthUpdateCallback?: (colWidth: number) => number; + initCallback?: (gridster: GridsterComponentInterface) => void; + destroyCallback?: (gridster: GridsterComponentInterface) => void; + gridSizeChangedCallback?: (gridster: GridsterComponentInterface) => void; diff --git a/ui-ngx/src/app/app.module.ts b/ui-ngx/src/app/app.module.ts index e18c909c9a..1ebd87c119 100644 --- a/ui-ngx/src/app/app.module.ts +++ b/ui-ngx/src/app/app.module.ts @@ -27,6 +27,23 @@ import { AppComponent } from './app.component'; import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.module'; import { RouterModule, Routes } from '@angular/router'; +import { DefaultUrlSerializer, UrlSerializer, UrlTree } from '@angular/router'; + +export default class TbUrlSerializer implements UrlSerializer { + private _defaultUrlSerializer: DefaultUrlSerializer = new DefaultUrlSerializer(); + + parse(url: string): UrlTree { + // Encode parentheses + url = url.replace(/\(/g, '%28').replace(/\)/g, '%29'); + // Use the default serializer. + return this._defaultUrlSerializer.parse(url) + } + + serialize(tree: UrlTree): string { + return this._defaultUrlSerializer.serialize(tree).replace(/%28/g, '(').replace(/%29/g, ')'); + } +} + const routes: Routes = [ { path: '**', redirectTo: 'home' @@ -55,7 +72,9 @@ export class PageNotFoundRoutingModule { } DashboardRoutingModule, PageNotFoundRoutingModule ], - providers: [], + providers: [ + { provide: UrlSerializer, useClass: TbUrlSerializer } + ], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 1fcd18bd93..0300c89d7f 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -23,7 +23,7 @@ import { DatasourceType, KeyInfo, LegendConfig, - LegendData, TargetDevice, + LegendData, TargetDevice, WidgetAction, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; @@ -95,6 +95,7 @@ export interface WidgetActionsApi { getActionDescriptors: (actionSourceId: string) => Array; handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; + onWidgetAction: ($event: Event, action: WidgetAction) => void; elementClick: ($event: Event) => void; cardClick: ($event: Event) => void; click: ($event: Event) => void; diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index b5b2b0e796..7446d67601 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -427,11 +427,11 @@ export class EntityService { break; case EntityType.WIDGETS_BUNDLE: pageLink.sortOrder.property = 'title'; - entitiesObservable = this.widgetService.getWidgetBundles(pageLink, false, true, config); + entitiesObservable = this.widgetService.getWidgetBundles(pageLink, false, true, false, config); break; case EntityType.WIDGET_TYPE: pageLink.sortOrder.property = 'name'; - entitiesObservable = this.widgetService.getWidgetTypes(pageLink, true, false, DeprecatedFilter.ALL, null, config); + entitiesObservable = this.widgetService.getWidgetTypes(pageLink, true, false, false, DeprecatedFilter.ALL, null, config); break; case EntityType.NOTIFICATION_TARGET: pageLink.sortOrder.property = 'name'; diff --git a/ui-ngx/src/app/core/http/image.service.ts b/ui-ngx/src/app/core/http/image.service.ts index 2339801e2b..eb15b658e5 100644 --- a/ui-ngx/src/app/core/http/image.service.ts +++ b/ui-ngx/src/app/core/http/image.service.ts @@ -25,11 +25,11 @@ import { ImageResourceInfo, imageResourceType, ImageResourceType, - IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix + IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix, ResourceSubType } from '@shared/models/resource.models'; import { catchError, map, switchMap } from 'rxjs/operators'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { blobToBase64 } from '@core/utils'; +import { blobToBase64, blobToText } from '@core/utils'; import { ResourcesService } from '@core/services/resources.service'; @Injectable({ @@ -46,13 +46,14 @@ export class ImageService { ) { } - public uploadImage(file: File, title: string, config?: RequestConfig): Observable { + public uploadImage(file: File, title: string, imageSubType: ResourceSubType, config?: RequestConfig): Observable { if (!config) { config = {}; } const formData = new FormData(); formData.append('file', file); formData.append('title', title); + formData.append('imageSubType', imageSubType); return this.http.post('/api/image', formData, defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); } @@ -81,9 +82,10 @@ export class ImageService { imageInfo, defaultHttpOptionsFromConfig(config)); } - public getImages(pageLink: PageLink, includeSystemImages = false, config?: RequestConfig): Observable> { + public getImages(pageLink: PageLink, imageSubType: ResourceSubType, + includeSystemImages = false, config?: RequestConfig): Observable> { return this.http.get>( - `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&includeSystemImages=${includeSystemImages}`, + `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&imageSubType=${imageSubType}&includeSystemImages=${includeSystemImages}`, defaultHttpOptionsFromConfig(config)); } @@ -130,6 +132,33 @@ export class ImageService { ); } + public getImageString(imageUrl: string): Observable { + imageUrl = removeTbImagePrefix(imageUrl); + let request: ReplaySubject; + if (this.imagesLoading[imageUrl]) { + request = this.imagesLoading[imageUrl]; + } else { + request = new ReplaySubject(1); + this.imagesLoading[imageUrl] = request; + const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}); + this.http.get(imageUrl, {...options, ...{ responseType: 'blob' } }).subscribe({ + next: (value) => { + request.next(value); + request.complete(); + }, + error: err => { + request.error(err); + }, + complete: () => { + delete this.imagesLoading[imageUrl]; + } + }); + } + return request.pipe( + switchMap(val => blobToText(val)) + ); + } + public resolveImageUrl(imageUrl: string, preview = false, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable { imageUrl = removeTbImagePrefix(imageUrl); if (isImageResourceUrl(imageUrl)) { diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 6f455189a7..b172b61a1d 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -86,9 +86,9 @@ export class WidgetService { } public getWidgetBundles(pageLink: PageLink, fullSearch = false, - tenantOnly = false, config?: RequestConfig): Observable> { + tenantOnly = false, scadaFirst = false, config?: RequestConfig): Observable> { return this.http.get>( - `/api/widgetsBundles${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}`, + `/api/widgetsBundles${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}&scadaFirst=${scadaFirst}`, defaultHttpOptionsFromConfig(config) ); } @@ -240,10 +240,13 @@ export class WidgetService { } public getWidgetTypes(pageLink: PageLink, tenantOnly = false, - fullSearch = false, deprecatedFilter = DeprecatedFilter.ALL, widgetTypes: Array = null, + fullSearch = false, scadaFirst = false, + deprecatedFilter = DeprecatedFilter.ALL, + widgetTypes: Array = null, config?: RequestConfig): Observable> { let url = - `/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}&deprecatedFilter=${deprecatedFilter}`; + `/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch} + &scadaFirst=${scadaFirst}&deprecatedFilter=${deprecatedFilter}`; if (widgetTypes && widgetTypes.length) { url += `&widgetTypeList=${widgetTypes.join(',')}`; } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index ae4d2e5c41..12a7e687bd 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -50,6 +50,7 @@ import { EntityId } from '@app/shared/models/id/entity-id'; import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models'; @Injectable({ providedIn: 'root' @@ -332,6 +333,27 @@ export class DashboardUtilsService { return widgetConfig; } + public prepareWidgetForScadaLayout(widget: Widget): Widget { + const config = widget.config; + config.showTitle = false; + config.dropShadow = false; + config.padding = '0'; + config.margin = '0'; + config.backgroundColor = 'rgba(0,0,0,0)'; + const settings = config.settings || {}; + settings.padding = '0'; + const background = settings.background; + if (isBackgroundSettings(background)) { + background.type = BackgroundType.color; + background.color = 'rgba(0,0,0,0)'; + background.overlay.enabled = false; + } else { + settings.background = colorBackground('rgba(0,0,0,0)'); + } + config.settings = settings; + return widget; + } + public validateAndUpdateDatasources(datasources?: Datasource[]): Datasource[] { if (!datasources) { datasources = []; @@ -662,7 +684,6 @@ export class DashboardUtilsService { const columns = gridSettings.columns || 24; const ratio = columns / prevColumns; layout.gridSettings = gridSettings; - let maxRow = 0; for (const w of Object.keys(layout.widgets)) { const widget = layout.widgets[w]; if (!widget.sizeX) { @@ -671,23 +692,23 @@ export class DashboardUtilsService { if (!widget.sizeY) { widget.sizeY = 1; } - maxRow = Math.max(maxRow, widget.row + widget.sizeY); } - const newMaxRow = Math.round(maxRow * ratio); for (const w of Object.keys(layout.widgets)) { const widget = layout.widgets[w]; - if (widget.row + widget.sizeY === maxRow) { - widget.row = Math.round(widget.row * ratio); - widget.sizeY = newMaxRow - widget.row; - } else { - widget.row = Math.round(widget.row * ratio); - widget.sizeY = Math.round(widget.sizeY * ratio); - } - widget.sizeX = Math.round(widget.sizeX * ratio); + widget.row = Math.round(widget.row * ratio); widget.col = Math.round(widget.col * ratio); - if (widget.col + widget.sizeX > columns) { - widget.sizeX = columns - widget.col; - } + widget.sizeX = Math.round(widget.sizeX * ratio); + widget.sizeY = Math.round(widget.sizeY * ratio); + } + } + + public moveWidgets(layout: DashboardLayout, cols: number, rows: number) { + cols = isDefinedAndNotNull(cols) ? Math.round(cols) : 0; + rows = isDefinedAndNotNull(rows) ? Math.round(rows) : 0; + for (const w of Object.keys(layout.widgets)) { + const widget = layout.widgets[w]; + widget.col = Math.max(0, widget.col + cols); + widget.row = Math.max(0, widget.row + rows); } } diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index ffb5b5f0a6..309d82ebc0 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -150,6 +150,13 @@ export class MenuService { path: '/resources/images', icon: 'filter' }, + { + id: 'scada_symbols', + name: 'scada.symbols', + type: 'link', + path: '/resources/scada-symbols', + icon: 'view_in_ar' + }, { id: 'resources_library', name: 'resource.resources-library', @@ -549,6 +556,13 @@ export class MenuService { path: '/resources/images', icon: 'filter' }, + { + id: 'scada_symbols', + name: 'scada.symbols', + type: 'link', + path: '/resources/scada-symbols', + icon: 'view_in_ar' + }, { id: 'resources_library', name: 'resource.resources-library', diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index a534f157b7..3f27e8289d 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -217,6 +217,23 @@ export const blobToBase64 = (blob: Blob): Observable => from(new Promise } )); +export const blobToText = (blob: Blob): Observable => from(new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.readAsText(blob); + } +)); + +export const updateFileContent = (file: File, newContent: string): File => { + const blob = new Blob([newContent], { type: file.type }); + return new File([blob], file.name, {type: file.type}); +}; + +export const createFileFromContent = (content: string, name: string, type: string): File => { + const blob = new Blob([content], { type }); + return new File([blob], name, { type }); +}; + const scrollRegex = /(auto|scroll)/; function parentNodes(node: Node, nodes: Node[]): Node[] { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index c7f8980426..8baaa67ead 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -44,6 +44,7 @@ [dashboard]="dashboard" [widget]="widget" [hideHeader]="hideHeader" + [scada]="data.scada" isAdd formControlName="widgetConfig"> diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 2089c03d5d..662739295b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -37,6 +37,7 @@ export interface AddWidgetDialogData { stateController: IStateController; widget: Widget; widgetInfo: WidgetInfo; + scada: boolean; } @Component({ diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index e923036571..21a99f70e9 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -380,6 +380,7 @@ [widgetEditMode]="widgetEditMode" [widget]="editingWidget" [widgetLayout]="editingWidgetLayout" + [scada]="editingLayoutCtx?.gridSettings?.layoutType === LayoutType.scada" (revertWidgetConfig)="onRevertWidgetEdit()" (applyWidgetConfig)="saveWidget()"> @@ -442,6 +443,7 @@ *ngIf="isAddingWidget" [aliasController]="dashboardCtx.aliasController" [filterWidgetTypes]="filterWidgetTypes" + [scadaFirst]="layouts.main.layoutCtx.gridSettings.layoutType === LayoutType.scada" (widgetSelected)="addWidgetFromType($event)"> diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 70022fff1b..d16e8f360f 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -51,6 +51,7 @@ import { DashboardStateLayouts, GridSettings, LayoutDimension, + LayoutType, WidgetLayout } from '@app/shared/models/dashboard.models'; import { WINDOW } from '@core/services/window.service'; @@ -150,6 +151,10 @@ import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-pag import { TbPopoverComponent } from '@shared/components/popover.component'; import { ResizeObserver } from '@juggle/resize-observer'; import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; +import { + MoveWidgetsDialogComponent, + MoveWidgetsDialogResult +} from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; // @dynamic @Component({ @@ -161,6 +166,8 @@ import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; }) export class DashboardPageComponent extends PageComponent implements IDashboardController, HasDirtyFlag, OnInit, AfterViewInit, OnDestroy { + LayoutType = LayoutType; + private forcePristine = false; get isDirty(): boolean { @@ -285,7 +292,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC gridSettings: {}, ignoreLoading: true, ctrl: null, - dashboardCtrl: this + dashboardCtrl: this, + displayGrid: 'onDrag&Resize' } }, right: { @@ -297,7 +305,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC gridSettings: {}, ignoreLoading: true, ctrl: null, - dashboardCtrl: this + dashboardCtrl: this, + displayGrid: 'onDrag&Resize' } } }; @@ -913,6 +922,28 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }); } + private moveWidgets($event: Event, layoutId: DashboardLayoutId) { + if ($event) { + $event.stopPropagation(); + } + this.layouts[layoutId].layoutCtx.displayGrid = 'always'; + this.cd.markForCheck(); + this.dialog.open(MoveWidgetsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe((result) => { + this.layouts[layoutId].layoutCtx.displayGrid = 'onDrag&Resize'; + if (result) { + const targetLayout = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts[layoutId]; + this.dashboardUtils.moveWidgets(targetLayout, result.cols, result.rows); + this.updateLayouts(); + } else { + this.cd.markForCheck(); + } + }); + } + private updateDashboardLayouts(newLayouts: DashboardStateLayouts) { this.dashboardUtils.setLayouts(this.dashboard, this.dashboardCtx.state, newLayouts); this.updateLayouts(); @@ -1163,6 +1194,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.runChangeDetection(); } + private isAddingToScadaLayout(): boolean { + const layouts = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts; + let layoutIds: DashboardLayoutId[]; + if (this.addingLayoutCtx?.id) { + layoutIds = [this.addingLayoutCtx?.id]; + } else { + layoutIds = Object.keys(layouts) as DashboardLayoutId[]; + } + return layoutIds.every(id => layouts[id].gridSettings.layoutType === LayoutType.scada); + } + private selectTargetLayout(): Observable { const layouts = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts; const layoutIds = Object.keys(layouts); @@ -1208,6 +1250,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC col: 0 }; newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget); + const scada = this.isAddingToScadaLayout(); + if (scada) { + newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget); + } if (widgetTypeInfo.typeParameters.useCustomDatasources) { this.addWidgetToDashboard(newWidget); } else { @@ -1222,7 +1268,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC aliasController: this.dashboardCtx.aliasController, stateController: this.dashboardCtx.stateController, widget: newWidget, - widgetInfo: widgetTypeInfo + widgetInfo: widgetTypeInfo, + scada } }).afterClosed().subscribe((addedWidget) => { if (addedWidget) { @@ -1355,6 +1402,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget, widgetTitle); } + dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext) { + if (this.isEdit && !this.isEditingWidget) { + layoutCtx.ctrl.resetHighlight(); + } + } + widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { if (this.isEditingWidget) { this.editWidget($event, layoutCtx, widget); @@ -1408,6 +1461,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC shortcut: 'M-I' } ); + dashboardContextActions.push( + { + action: ($event) => { + this.moveWidgets($event, layoutCtx.id); + }, + enabled: true, + value: 'dashboard.move-all-widgets', + icon: 'open_with' + } + ); } return dashboardContextActions; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts index b2ed609c29..0c43be1e91 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts @@ -21,6 +21,7 @@ import { IAliasController, IStateController } from '@core/api/widget-api.models' import { ILayoutController } from './layout/layout.models'; import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models'; import { Observable } from 'rxjs'; +import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface'; export declare type DashboardPageScope = 'tenant' | 'customer'; @@ -52,6 +53,7 @@ export interface IDashboardController { exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string); removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); + dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext); widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array; prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array; @@ -69,6 +71,7 @@ export interface DashboardPageLayoutContext { ctrl: ILayoutController; dashboardCtrl: IDashboardController; ignoreLoading: boolean; + displayGrid: displayGrids; } export interface DashboardPageLayout { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index c50fbaef11..d15512b561 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -27,129 +27,181 @@ -
-
-
- - dashboard.state-controller +
+
+
+
{{ 'dashboard.state-controller' | translate }}
+ {{ getStatesControllerTranslation(stateControllerId) }} -
- dashboard.title-settings - - {{ 'dashboard.display-title' | translate }} +
+
+
dashboard.title-settings
+
+ + {{ 'dashboard.title' | translate }} - -
-
- dashboard.dashboard-logo-settings - +
+ +
+
dashboard.dashboard-logo-settings
+
+ {{ 'dashboard.display-dashboard-logo' | translate }} - - - -
- dashboard.toolbar-settings - - {{ 'dashboard.hide-toolbar' | translate }} - - - {{ 'dashboard.toolbar-always-open' | translate }} - - - {{ 'dashboard.display-dashboards-selection' | translate }} - - - {{ 'dashboard.display-entities-selection' | translate }} - - - {{ 'dashboard.display-filters' | translate }} - - - {{ 'dashboard.display-dashboard-timewindow' | translate }} - - - {{ 'dashboard.display-dashboard-export' | translate }} - - - {{ 'dashboard.display-update-dashboard-image' | translate }} - -
- - - - dashboard.advanced-settings - - - - - - +
+ + +
+
+
dashboard.toolbar-settings
+ + {{ 'dashboard.hide-toolbar' | translate }} + + + {{ 'dashboard.toolbar-always-open' | translate }} + + + {{ 'dashboard.display-dashboards-selection' | translate }} + + + {{ 'dashboard.display-entities-selection' | translate }} + + + {{ 'dashboard.display-filters' | translate }} + + + {{ 'dashboard.display-dashboard-timewindow' | translate }} + + + {{ 'dashboard.display-dashboard-export' | translate }} + + + {{ 'dashboard.display-update-dashboard-image' | translate }} +
-
-
- dashboard.layout-settings - - dashboard.columns-count - - - {{ 'dashboard.columns-count-required' | translate }} - - - {{ 'dashboard.min-columns-count-message' | translate }} - - - {{ 'dashboard.max-columns-count-message' | translate }} - + + + + dashboard.advanced-settings + + + + + + +
+
+
+
dashboard.layout
+ + + + {{ layoutTypeTranslations.get(type) | translate }} + + + +
+
+
dashboard.layout-settings
+
+
dashboard.columns-count
+ + + + warning + + + + + + {{ columns }} + + - - dashboard.widgets-margins - - - {{ 'dashboard.margin-required' | translate }} - - - {{ 'dashboard.min-margin-message' | translate }} - - - {{ 'dashboard.max-margin-message' | translate }} - +
+
+
dashboard.min-layout-width
+ + + + warning + + dashboard.columns-suffix - +
+
+
dashboard.widgets-margins
+ + + + warning + + +
+
+ {{ 'dashboard.apply-outer-margin' | translate }} - +
+
+ {{ 'dashboard.autofill-height' | translate }} - +
+
dashboard.background-color
+ - - - - dashboard.background-size-mode +
+ + +
+
{{ 'dashboard.background-size-mode' | translate }}
+ Fit width Fit height @@ -158,34 +210,41 @@ Original size - -
- dashboard.mobile-layout - +
+
+
+
dashboard.mobile-layout
+
+ {{ 'dashboard.display-first-in-mobile-view' | translate }} -
- - {{ 'dashboard.autofill-height' | translate }} - - - dashboard.mobile-row-height - - - {{ 'dashboard.mobile-row-height-required' | translate }} - - - {{ 'dashboard.min-mobile-row-height-message' | translate }} - - - {{ 'dashboard.max-mobile-row-height-message' | translate }} - - -
- +
+
+ + {{ 'dashboard.autofill-height' | translate }} + +
+
+
dashboard.mobile-row-height
+ + + + warning + + px + +
- +
+ + + +
+
+
dashboard.move-by
+
+ + + dashboard.cols + + + + dashboard.rows + +
+
+
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.ts new file mode 100644 index 0000000000..ba030c467d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.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 { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; + +export interface MoveWidgetsDialogResult { + cols: number; + rows: number; +} + +@Component({ + selector: 'tb-move-widgets-dialog', + templateUrl: './move-widgets-dialog.component.html', + providers: [], + styleUrls: [] +}) +export class MoveWidgetsDialogComponent extends DialogComponent { + + moveWidgetsFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + private fb: UntypedFormBuilder, + private dialog: MatDialog) { + super(store, router, dialogRef); + + this.moveWidgetsFormGroup = this.fb.group({ + cols: [0, [Validators.required]], + rows: [0, [Validators.required]] + } + ); + } + + cancel(): void { + this.dialogRef.close(null); + } + + move(): void { + const result: MoveWidgetsDialogResult = this.moveWidgetsFormGroup.value; + this.dialogRef.close(result); + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index c69a2a4de9..6469429295 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -23,7 +23,10 @@
-
+
this.dashboardWidgets.sortWidgets(), itemInitCallback: (item, itemComponent) => { (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; + }, + colWidthUpdateCallback: (colWidth) => { + if (this.colWidthInteger) { + return Math.floor(colWidth); + } else { + return colWidth; + } } }; - this.updateMobileOpts(); + this.updateGridOpts(); this.breakpointObserverSubscription = this.breakpointObserver .observe(MediaBreakpoints['gt-sm']).subscribe( () => { - this.updateMobileOpts(); + this.updateGridOpts(); this.notifyGridsterOptionsChanged(); } ); @@ -266,20 +302,24 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } ngOnChanges(changes: SimpleChanges): void { - let updateMobileOpts = false; - let updateLayoutOpts = false; + let updateGridOpts = false; + let updateColumnOpts = false; let updateEditingOpts = false; + let updateDisplayGridOpts = false; let updateWidgets = false; let updateDashboardTimewindow = false; for (const propName of Object.keys(changes)) { const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { - if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) { - updateMobileOpts = true; + if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', + 'mobileRowHeight', 'colWidthInteger'].includes(propName)) { + updateGridOpts = true; } else if (['outerMargin', 'margin', 'columns'].includes(propName)) { - updateLayoutOpts = true; - } else if (propName === 'isEdit') { + updateColumnOpts = true; + } else if (['isEdit', 'isEditingWidget'].includes(propName)) { updateEditingOpts = true; + } else if (propName === 'displayGrid') { + updateDisplayGridOpts = true; } else if (['widgets', 'widgetLayouts'].includes(propName)) { updateWidgets = true; } else if (propName === 'dashboardTimewindow') { @@ -293,16 +333,19 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); } - if (updateLayoutOpts) { - this.updateLayoutOpts(); + if (updateColumnOpts) { + this.updateColumnOpts(); } - if (updateMobileOpts) { - this.updateMobileOpts(); + if (updateGridOpts) { + this.updateGridOpts(); } if (updateEditingOpts) { this.updateEditingOpts(); } - if (updateMobileOpts || updateLayoutOpts || updateEditingOpts) { + if (updateDisplayGridOpts) { + this.updateDisplayGridOpts(); + } + if (updateGridOpts || updateColumnOpts || updateEditingOpts || updateDisplayGridOpts) { this.notifyGridsterOptionsChanged(); } } @@ -352,6 +395,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } + onDashboardMouseDown($event: MouseEvent) { + if (this.callbacks && this.callbacks.onDashboardMouseDown) { + if ($event) { + $event.stopPropagation(); + } + this.callbacks.onDashboardMouseDown($event); + } + } + openDashboardContextMenu($event: MouseEvent) { if (this.callbacks && this.callbacks.prepareDashboardContextMenu) { const items = this.callbacks.prepareDashboardContextMenu($event); @@ -519,7 +571,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo }); } - private updateMobileOpts(parentHeight?: number) { + private onGridsterParentResize() { + const parentHeight = this.gridster.el.offsetHeight; + if (this.isMobileSize && this.mobileAutofillHeight && parentHeight) { + this.updateGridOpts(parentHeight); + } + this.notifyGridsterOptionsChanged(); + } + + private updateGridOpts(parentHeight?: number) { let updateWidgetRowsAndSort = false; const isMobileSize = this.checkIsMobileSize(); if (this.isMobileSize !== isMobileSize) { @@ -530,7 +590,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo if (autofillHeight) { this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : GridType.Fit; } else { - this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : GridType.ScrollVertical; + this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : this.gridType || GridType.ScrollVertical; } const mobileBreakPoint = this.isMobileSize ? 20000 : 0; this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; @@ -543,23 +603,19 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } - private onGridsterParentResize() { - const parentHeight = this.gridster.el.offsetHeight; - if (this.isMobileSize && this.mobileAutofillHeight && parentHeight) { - this.updateMobileOpts(parentHeight); - } - this.notifyGridsterOptionsChanged(); - } - - private updateLayoutOpts() { + private updateColumnOpts() { this.gridsterOpts.minCols = this.columns ? this.columns : 24; this.gridsterOpts.outerMargin = isDefined(this.outerMargin) ? this.outerMargin : true; this.gridsterOpts.margin = isDefined(this.margin) ? this.margin : 10; } private updateEditingOpts() { - this.gridsterOpts.resizable.enabled = this.isEdit; - this.gridsterOpts.draggable.enabled = this.isEdit; + this.gridsterOpts.resizable.enabled = this.isEdit && !this.isEditingWidget; + this.gridsterOpts.draggable.enabled = this.isEdit && !this.isEditingWidget; + } + + private updateDisplayGridOpts() { + this.gridsterOpts.displayGrid = this.displayGrid; } public notifyGridsterOptionsChanged() { diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.html b/ui-ngx/src/app/modules/home/components/details-panel.component.html index d3e50ca1d7..2483a5b5e3 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.html @@ -40,7 +40,7 @@ search -
diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.ts b/ui-ngx/src/app/modules/home/components/details-panel.component.ts index bdcfbd636b..3b536c809e 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.ts @@ -28,6 +28,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { UntypedFormGroup } from '@angular/forms'; import { Subscription } from 'rxjs'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-details-panel', @@ -44,6 +45,10 @@ export class DetailsPanelComponent extends PageComponent implements OnDestroy { @Input() isShowSearch = false; @Input() backgroundColor = '#FFF'; + @Input() + @coerceBoolean() + showCloseDetails = true; + private theFormValue: UntypedFormGroup; private formSubscription: Subscription = null; diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 5390a915d3..6678497820 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -124,7 +124,10 @@ import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-dow import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component'; import { AlarmDurationPredicateValueComponent } from '@home/components/profile/alarm/alarm-duration-predicate-value.component'; import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component'; -import { WidgetContainerComponent } from '@home/components/widget/widget-container.component'; +import { + EditWidgetActionsTooltipComponent, + WidgetContainerComponent +} from '@home/components/widget/widget-container.component'; import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snmp/snmp-device-profile-transport.module'; import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module'; import { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.module'; @@ -171,6 +174,7 @@ import { import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; +import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; @NgModule({ declarations: @@ -207,6 +211,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet EntityAliasDialogComponent, DashboardComponent, WidgetContainerComponent, + EditWidgetActionsTooltipComponent, WidgetComponent, WidgetConfigComponent, WidgetPreviewComponent, @@ -283,6 +288,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet EditWidgetComponent, DashboardWidgetSelectComponent, AddWidgetDialogComponent, + MoveWidgetsDialogComponent, ManageDashboardLayoutsDialogComponent, DashboardSettingsDialogComponent, ManageDashboardStatesDialogComponent, @@ -345,6 +351,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet EntityAliasDialogComponent, DashboardComponent, WidgetContainerComponent, + EditWidgetActionsTooltipComponent, WidgetComponent, WidgetConfigComponent, WidgetPreviewComponent, @@ -414,6 +421,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet EditWidgetComponent, DashboardWidgetSelectComponent, AddWidgetDialogComponent, + MoveWidgetsDialogComponent, ManageDashboardLayoutsDialogComponent, DashboardSettingsDialogComponent, ManageDashboardStatesDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 06af5402ca..a32d7c2311 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -142,6 +142,7 @@ import { import { UnreadNotificationBasicConfigComponent } from '@home/components/widget/config/basic/cards/unread-notification-basic-config.component'; +import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/basic/scada/scada-symbol-basic-config.component'; @NgModule({ declarations: [ @@ -189,7 +190,8 @@ import { MobileAppQrCodeBasicConfigComponent, LabelCardBasicConfigComponent, LabelValueCardBasicConfigComponent, - UnreadNotificationBasicConfigComponent + UnreadNotificationBasicConfigComponent, + ScadaSymbolBasicConfigComponent ], imports: [ CommonModule, @@ -235,6 +237,7 @@ import { BarChartBasicConfigComponent, PolarAreaChartBasicConfigComponent, RadarChartBasicConfigComponent, + ScadaSymbolBasicConfigComponent, DigitalSimpleGaugeBasicConfigComponent, MobileAppQrCodeBasicConfigComponent, LabelCardBasicConfigComponent, @@ -283,5 +286,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type
widgets.action-button.on-click
@@ -38,9 +38,9 @@
widgets.button-state.activated-state
widgets.button-state.disabled-state
widgets.command-button.on-click
widgets.button-state.disabled-state
widgets.rpc-state.initial-state
widgets.power-button.power-on
widgets.power-button.power-off
widgets.rpc-state.disabled-state
widgets.rpc-state.initial-state
widgets.toggle-button.check
widgets.toggle-button.uncheck
widgets.rpc-state.disabled-state
!!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/basic/indicator/status-widget-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html index 57eb184009..52563a1b46 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.html @@ -22,11 +22,11 @@
widgets.rpc-state.initial-state
widgets.rpc-state.disabled-state
widgets.rpc-state.initial-state
widgets.rpc-state.turn-on
widgets.rpc-state.turn-off
widgets.rpc-state.disabled-state
widgets.slider.initial-value
widgets.slider.on-value-change
widgets.rpc-state.disabled-state
+ + +
+
scada.symbol.symbol
+ +
+ +
+
+ + {{ 'widget-config.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widget-config.card-icon' | translate }} + +
+ + + + + + + + +
+
+
+
+
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.ts new file mode 100644 index 0000000000..42b0dae3d8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-symbol-basic-config.component.ts @@ -0,0 +1,157 @@ +/// +/// 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 { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { + scadaSymbolWidgetDefaultSettings, + ScadaSymbolWidgetSettings +} from '@home/components/widget/lib/scada/scada-symbol-widget.models'; +import { isUndefined } from '@core/utils'; +import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-scada-symbol-basic-config', + templateUrl: './scada-symbol-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class ScadaSymbolBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + return this.scadaSymbolWidgetConfigForm.get('targetDevice').value; + } + + scadaSymbolWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.scadaSymbolWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: ScadaSymbolWidgetSettings = {...scadaSymbolWidgetDefaultSettings, ...(configData.config.settings || {})}; + const iconSize = resolveCssSize(configData.config.iconSize); + this.scadaSymbolWidgetConfigForm = this.fb.group({ + targetDevice: [configData.config.targetDevice, []], + scadaSymbolUrl: [settings.scadaSymbolUrl, []], + scadaSymbolObjectSettings: [settings.scadaSymbolObjectSettings, []], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + + showIcon: [configData.config.showTitleIcon, []], + iconSize: [iconSize[0], [Validators.min(0)]], + iconSizeUnit: [iconSize[1], []], + icon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + padding: [settings.padding, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.targetDevice = config.targetDevice; + + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showIcon; + this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); + this.widgetConfig.config.titleIcon = config.icon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.scadaSymbolUrl = config.scadaSymbolUrl; + this.widgetConfig.config.settings.scadaSymbolObjectSettings = config.scadaSymbolObjectSettings; + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + this.widgetConfig.config.settings.padding = config.padding; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showIcon']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.scadaSymbolWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.scadaSymbolWidgetConfigForm.get('showIcon').value; + + if (showTitle) { + this.scadaSymbolWidgetConfigForm.get('title').enable(); + this.scadaSymbolWidgetConfigForm.get('titleFont').enable(); + this.scadaSymbolWidgetConfigForm.get('titleColor').enable(); + this.scadaSymbolWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.scadaSymbolWidgetConfigForm.get('iconSize').enable(); + this.scadaSymbolWidgetConfigForm.get('iconSizeUnit').enable(); + this.scadaSymbolWidgetConfigForm.get('icon').enable(); + this.scadaSymbolWidgetConfigForm.get('iconColor').enable(); + } else { + this.scadaSymbolWidgetConfigForm.get('iconSize').disable(); + this.scadaSymbolWidgetConfigForm.get('iconSizeUnit').disable(); + this.scadaSymbolWidgetConfigForm.get('icon').disable(); + this.scadaSymbolWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.scadaSymbolWidgetConfigForm.get('title').disable(); + this.scadaSymbolWidgetConfigForm.get('titleFont').disable(); + this.scadaSymbolWidgetConfigForm.get('titleColor').disable(); + this.scadaSymbolWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.scadaSymbolWidgetConfigForm.get('iconSize').disable(); + this.scadaSymbolWidgetConfigForm.get('iconSizeUnit').disable(); + this.scadaSymbolWidgetConfigForm.get('icon').disable(); + this.scadaSymbolWidgetConfigForm.get('iconColor').disable(); + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } +} 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/target-device.component.html b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html index ad2728484c..7adea38000 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html @@ -24,13 +24,13 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts index c1d09f50b6..58893cb2a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts @@ -60,6 +60,10 @@ export class TargetDeviceComponent implements ControlValueAccessor, OnInit, Vali return this.widgetConfigComponent.widgetConfigCallbacks; } + public get targetDeviceOptional(): boolean { + return this.widgetConfigComponent.modelValue?.typeParameters?.targetDeviceOptional; + } + targetDeviceType = TargetDeviceType; entityType = EntityType; @@ -103,9 +107,9 @@ export class TargetDeviceComponent implements ControlValueAccessor, OnInit, Vali ngOnInit() { this.targetDeviceFormGroup = this.fb.group({ - type: [null, !this.widgetEditMode ? [Validators.required] : []], - deviceId: [null, !this.widgetEditMode ? [Validators.required] : []], - entityAliasId: [null, !this.widgetEditMode ? [Validators.required] : []] + type: [null, (!this.widgetEditMode && !this.targetDeviceOptional) ? [Validators.required] : []], + deviceId: [null, (!this.widgetEditMode && !this.targetDeviceOptional) ? [Validators.required] : []], + entityAliasId: [null, (!this.widgetEditMode && !this.targetDeviceOptional) ? [Validators.required] : []] }); this.targetDeviceFormGroup.get('type').valueChanges.subscribe(() => { this.updateValidators(); 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/action/action-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts index 050637468d..b1eb79f2bb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts @@ -24,7 +24,7 @@ import { } from '@shared/models/telemetry/telemetry.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { BehaviorSubject, forkJoin, Observable, Observer, of, Subscription, throwError } from 'rxjs'; -import { catchError, delay, map, share, take, tap } from 'rxjs/operators'; +import { catchError, delay, map, share, take } from 'rxjs/operators'; import { UtilsService } from '@core/services/utils.service'; import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { @@ -45,6 +45,7 @@ import { import { ValueType } from '@shared/models/constants'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityId } from '@shared/models/id/entity-id'; +import { isDefinedAndNotNull } from '@core/utils'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix @@ -117,14 +118,16 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A } } }; - const valueGetter = ValueGetter.fromSettings(this.ctx, getValueSettings, valueType, observer); + const simulated = this.ctx.utilsService.widgetEditMode || this.ctx.isPreview; + const valueGetter = ValueGetter.fromSettings(this.ctx, getValueSettings, valueType, observer, simulated); this.valueGetters.push(valueGetter); this.valueActions.push(valueGetter); return valueGetter; } protected createValueSetter(setValueSettings: SetValueSettings): ValueSetter { - const valueSetter = ValueSetter.fromSettings(this.ctx, setValueSettings); + const simulated = this.ctx.utilsService.widgetEditMode || this.ctx.isPreview; + const valueSetter = ValueSetter.fromSettings(this.ctx, setValueSettings, simulated); this.valueActions.push(valueSetter); return valueSetter; } @@ -229,22 +232,22 @@ export abstract class ValueGetter extends ValueAction { static fromSettings(ctx: WidgetContext, settings: GetValueSettings, valueType: ValueType, - valueObserver: Partial>): ValueGetter { + valueObserver: Partial>, + simulated: boolean): ValueGetter { switch (settings.action) { case GetValueAction.DO_NOTHING: - return new DefaultValueGetter(ctx, settings, valueType, valueObserver); + return new DefaultValueGetter(ctx, settings, valueType, valueObserver, simulated); case GetValueAction.EXECUTE_RPC: - return new ExecuteRpcValueGetter(ctx, settings, valueType, valueObserver); + return new ExecuteRpcValueGetter(ctx, settings, valueType, valueObserver, simulated); case GetValueAction.GET_ATTRIBUTE: - return new AttributeValueGetter(ctx, settings, valueType, valueObserver); + return new AttributeValueGetter(ctx, settings, valueType, valueObserver, simulated); case GetValueAction.GET_TIME_SERIES: - return new TimeSeriesValueGetter(ctx, settings, valueType, valueObserver); + return new TimeSeriesValueGetter(ctx, settings, valueType, valueObserver, simulated); case GetValueAction.GET_DASHBOARD_STATE: - return new DashboardStateGetter(ctx, settings, valueType, valueObserver); + return new DashboardStateGetter(ctx, settings, valueType, valueObserver, simulated); } } - private readonly isSimulated: boolean; private readonly dataConverter: DataToValueConverter; private getValueSubscription: Subscription; @@ -252,16 +255,16 @@ export abstract class ValueGetter extends ValueAction { protected constructor(protected ctx: WidgetContext, protected settings: GetValueSettings, protected valueType: ValueType, - protected valueObserver: Partial>) { + protected valueObserver: Partial>, + protected simulated: boolean) { super(ctx, settings); - this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode; if (this.settings.action !== GetValueAction.DO_NOTHING) { this.dataConverter = new DataToValueConverter(settings.dataToValue, valueType); } } getValue(): Observable { - const valueObservable: Observable = (this.isSimulated ? of(null).pipe(delay(500)) : this.doGetValue()).pipe( + const valueObservable: Observable = this.doGetValue().pipe( map((data) => { if (this.dataConverter) { return this.dataConverter.dataToValue(data); @@ -346,29 +349,29 @@ export class ValueToDataConverter { export abstract class ValueSetter extends ValueAction { static fromSettings(ctx: WidgetContext, - settings: SetValueSettings): ValueSetter { + settings: SetValueSettings, + simulated: boolean): ValueSetter { switch (settings.action) { case SetValueAction.EXECUTE_RPC: - return new ExecuteRpcValueSetter(ctx, settings); + return new ExecuteRpcValueSetter(ctx, settings, simulated); case SetValueAction.SET_ATTRIBUTE: - return new AttributeValueSetter(ctx, settings); + return new AttributeValueSetter(ctx, settings, simulated); case SetValueAction.ADD_TIME_SERIES: - return new TimeSeriesValueSetter(ctx, settings); + return new TimeSeriesValueSetter(ctx, settings, simulated); } } - private readonly isSimulated: boolean; private readonly valueToDataConverter: ValueToDataConverter; protected constructor(protected ctx: WidgetContext, - protected settings: SetValueSettings) { + protected settings: SetValueSettings, + protected simulated: boolean) { super(ctx, settings); - this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode; this.valueToDataConverter = new ValueToDataConverter(settings.valueToData); } setValue(value: V): Observable { - if (this.isSimulated) { + if (this.simulated) { return of(null).pipe(delay(500)); } else { return this.doSetValue(this.valueToDataConverter.valueToData(value)).pipe( @@ -389,8 +392,9 @@ export class DefaultValueGetter extends ValueGetter { constructor(protected ctx: WidgetContext, protected settings: GetValueSettings, protected valueType: ValueType, - protected valueObserver: Partial>) { - super(ctx, settings, valueType, valueObserver); + protected valueObserver: Partial>, + protected simulated: boolean) { + super(ctx, settings, valueType, valueObserver, simulated); this.defaultValue = settings.defaultValue; } @@ -406,20 +410,26 @@ export class ExecuteRpcValueGetter extends ValueGetter { constructor(protected ctx: WidgetContext, protected settings: GetValueSettings, protected valueType: ValueType, - protected valueObserver: Partial>) { - super(ctx, settings, valueType, valueObserver); + protected valueObserver: Partial>, + protected simulated: boolean) { + super(ctx, settings, valueType, valueObserver, simulated); this.executeRpcSettings = settings.executeRpc; } protected doGetValue(): Observable { - return this.ctx.controlApi.sendTwoWayCommand(this.executeRpcSettings.method, null, - this.executeRpcSettings.requestTimeout, - this.executeRpcSettings.requestPersistent, - this.executeRpcSettings.persistentPollingInterval).pipe( + if (this.simulated) { + const defaultValue = isDefinedAndNotNull(this.settings.defaultValue) ? this.settings.defaultValue : null; + return of(defaultValue).pipe(delay(500)); + } else { + return this.ctx.controlApi.sendTwoWayCommand(this.executeRpcSettings.method, null, + this.executeRpcSettings.requestTimeout, + this.executeRpcSettings.requestPersistent, + this.executeRpcSettings.persistentPollingInterval).pipe( catchError((err) => { throw handleRpcError(this.ctx, err); }) - ); + ); + } } } @@ -431,24 +441,30 @@ export abstract class TelemetryValueGetter protected constructor(protected ctx: WidgetContext, protected settings: GetValueSettings, protected valueType: ValueType, - protected valueObserver: Partial>) { - super(ctx, settings, valueType, valueObserver); + protected valueObserver: Partial>, + protected simulated: boolean) { + super(ctx, settings, valueType, valueObserver, simulated); const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo(); this.targetEntityId = entityInfo?.entityId; } protected doGetValue(): Observable { - if (!this.targetEntityId && !this.ctx.defaultSubscription.rpcEnabled) { - return throwError(() => new Error(this.ctx.translate.instant('widgets.value-action.error.target-entity-is-not-set'))); - } - if (this.targetEntityId) { - const err = validateAttributeScope(this.ctx, this.targetEntityId, this.scope()); - if (err) { - return throwError(() => err); - } - return this.subscribeForTelemetryValue(); + if (this.simulated) { + const defaultValue = isDefinedAndNotNull(this.settings.defaultValue) ? this.settings.defaultValue : null; + return of(defaultValue).pipe(delay(100)); } else { - return of(null); + if (!this.targetEntityId && !this.ctx.defaultSubscription.rpcEnabled) { + return throwError(() => new Error(this.ctx.translate.instant('widgets.value-action.error.target-entity-is-not-set'))); + } + if (this.targetEntityId) { + const err = validateAttributeScope(this.ctx, this.targetEntityId, this.scope()); + if (err) { + return throwError(() => err); + } + return this.subscribeForTelemetryValue(); + } else { + return of(null); + } } } @@ -492,8 +508,9 @@ export class AttributeValueGetter extends TelemetryValueGetter, protected valueType: ValueType, - protected valueObserver: Partial>) { - super(ctx, settings, valueType, valueObserver); + protected valueObserver: Partial>, + protected simulated: boolean) { + super(ctx, settings, valueType, valueObserver, simulated); } protected getTelemetryValueSettings(): GetAttributeValueSettings { @@ -511,8 +528,9 @@ export class TimeSeriesValueGetter extends TelemetryValueGetter, protected valueType: ValueType, - protected valueObserver: Partial>) { - super(ctx, settings, valueType, valueObserver); + protected valueObserver: Partial>, + protected simulated: boolean) { + super(ctx, settings, valueType, valueObserver, simulated); } protected getTelemetryValueSettings(): TelemetryValueSettings { @@ -524,12 +542,17 @@ export class DashboardStateGetter extends ValueGetter { constructor(protected ctx: WidgetContext, protected settings: GetValueSettings, protected valueType: ValueType, - protected valueObserver: Partial>) { - super(ctx, settings, valueType, valueObserver); + protected valueObserver: Partial>, + protected simulated: boolean) { + super(ctx, settings, valueType, valueObserver, simulated); } protected doGetValue(): Observable { - return this.ctx.stateController.dashboardCtrl.dashboardCtx.stateId; + if (this.simulated) { + return of('default'); + } else { + return this.ctx.stateController.dashboardCtrl.dashboardCtx.stateId; + } } } @@ -538,8 +561,9 @@ export class ExecuteRpcValueSetter extends ValueSetter { private readonly executeRpcSettings: RpcSettings; constructor(protected ctx: WidgetContext, - protected settings: SetValueSettings) { - super(ctx, settings); + protected settings: SetValueSettings, + protected simulated: boolean) { + super(ctx, settings, simulated); this.executeRpcSettings = settings.executeRpc; } @@ -560,8 +584,9 @@ export abstract class TelemetryValueSetter extends ValueSetter { protected targetEntityId: EntityId; protected constructor(protected ctx: WidgetContext, - protected settings: SetValueSettings) { - super(ctx, settings); + protected settings: SetValueSettings, + protected simulated: boolean) { + super(ctx, settings, simulated); const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo(); this.targetEntityId = entityInfo?.entityId; } @@ -594,8 +619,9 @@ export class AttributeValueSetter extends TelemetryValueSetter { private readonly setAttributeValueSettings: SetAttributeValueSettings; constructor(protected ctx: WidgetContext, - protected settings: SetValueSettings) { - super(ctx, settings); + protected settings: SetValueSettings, + protected simulated: boolean) { + super(ctx, settings, simulated); this.setAttributeValueSettings = settings.setAttribute; } @@ -616,8 +642,9 @@ export class TimeSeriesValueSetter extends TelemetryValueSetter { private readonly putTimeSeriesValueSettings: TelemetryValueSettings; constructor(protected ctx: WidgetContext, - protected settings: SetValueSettings) { - super(ctx, settings); + protected settings: SetValueSettings, + protected simulated: boolean) { + super(ctx, settings, simulated); this.putTimeSeriesValueSettings = settings.putTimeSeries; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html index 5179d71c7e..aabab512e1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html @@ -17,7 +17,9 @@ -->
- + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts index def776897e..ed023c1a0d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts @@ -43,6 +43,7 @@ import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; import { mergeDeep } from '@core/utils'; +import { WidgetComponent } from '@home/components/widget/widget.component'; @Component({ selector: 'tb-time-series-chart-widget', @@ -84,7 +85,8 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV private timeSeriesChart: TbTimeSeriesChart; - constructor(private imagePipe: ImagePipe, + constructor(public widgetComponent: WidgetComponent, + private imagePipe: ImagePipe, private sanitizer: DomSanitizer, private renderer: Renderer2, private cd: ChangeDetectorRef) { 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 new file mode 100644 index 0000000000..da2b80c7ce --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.html @@ -0,0 +1,29 @@ + +
+
+ + + +
+
+
+
scada.no-symbol-selected
+
+ +
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 new file mode 100644 index 0000000000..c926e0a4b0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.scss @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-scada-symbol-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + padding: 20px 24px 24px 24px; + > div:not(.tb-scada-symbol-overlay) { + z-index: 1; + } + .tb-scada-symbol-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } + .tb-scada-symbol-content { + flex: 1; + min-width: 0; + min-height: 0; + .tb-scada-symbol-shape { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + .tb-no-scada-symbol { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + color: rgba(0, 0, 0, 0.87); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 16px; + } + } +} 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 new file mode 100644 index 0000000000..6db0d202c7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts @@ -0,0 +1,146 @@ +/// +/// 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 { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ScadaSymbolObject, ScadaSymbolObjectCallbacks } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { + scadaSymbolWidgetDefaultSettings, + ScadaSymbolWidgetSettings +} from '@home/components/widget/lib/scada/scada-symbol-widget.models'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; +import { ImageService } from '@core/http/image.service'; +import { WidgetComponent } from '@home/components/widget/widget.component'; +import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { catchError, share } from 'rxjs/operators'; + +@Component({ + selector: 'tb-scada-symbol-widget', + templateUrl: './scada-symbol-widget.component.html', + styleUrls: ['../action/action-widget.scss', './scada-symbol-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolWidgetComponent implements OnInit, AfterViewInit, OnDestroy, ScadaSymbolObjectCallbacks { + + @ViewChild('scadaSymbolShape', {static: false}) + scadaSymbolShape: ElementRef; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + private loadingSubject = new BehaviorSubject(false); + private settings: ScadaSymbolWidgetSettings; + private scadaSymbolContent$: Observable; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + padding: string; + + loading$ = this.loadingSubject.asObservable().pipe(share()); + + scadaSymbolObject: ScadaSymbolObject; + noScadaSymbol = false; + + constructor(public widgetComponent: WidgetComponent, + protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + private imageService: ImageService, + protected cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.actionWidget = this; + this.settings = mergeDeep({} as ScadaSymbolWidgetSettings, scadaSymbolWidgetDefaultSettings, this.ctx.settings || {}); + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; + + if (this.settings.scadaSymbolContent) { + this.scadaSymbolContent$ = of(this.settings.scadaSymbolContent); + } else if (this.settings.scadaSymbolUrl) { + this.scadaSymbolContent$ = this.imageService.getImageString(this.settings.scadaSymbolUrl) + .pipe(catchError(() => of('empty'))); + } else { + this.scadaSymbolContent$ = of('empty'); + } + } + + ngAfterViewInit(): void { + this.scadaSymbolContent$.subscribe((content) => { + this.initObject(this.scadaSymbolShape.nativeElement, content); + }); + } + + ngOnDestroy() { + if (this.scadaSymbolObject) { + this.scadaSymbolObject.destroy(); + } + this.loadingSubject.complete(); + this.loadingSubject.unsubscribe(); + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + onScadaSymbolObjectLoadingState(loading: boolean) { + this.loadingSubject.next(loading); + } + + onScadaSymbolObjectError(error: string) { + this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true); + } + + onScadaSymbolObjectMessage(message: string) { + this.ctx.showSuccessToast(message, 3000, 'bottom', 'center', this.ctx.toastTargetId, true); + } + + private initObject(rootElement: HTMLElement, + content: string) { + const simulated = this.ctx.utilsService.widgetEditMode || + this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false); + if (content.startsWith(' string; + formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; + text: (element: Element | Element[], text: string) => void; + font: (element: Element | Element[], font: Font, color: string) => void; + animate: (element: Element, duration: number) => Runner; + resetAnimation: (element: Element) => void; + finishAnimation: (element: Element) => void; + disable: (element: Element | Element[]) => void; + enable: (element: Element | Element[]) => void; + callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial>) => void; + setValue: (valueId: string, value: any) => void; +} + +export interface ScadaSymbolContext { + api: ScadaSymbolApi; + tags: {[id: string]: Element[]}; + values: {[id: string]: any}; + properties: {[id: string]: any}; + svg: Svg; +} + +export type ScadaSymbolStateRenderFunction = (ctx: ScadaSymbolContext, svg: Svg) => void; + +export type ScadaSymbolTagStateRenderFunction = (ctx: ScadaSymbolContext, element: Element) => void; + +export type ScadaSymbolActionTrigger = 'click'; + +export type ScadaSymbolActionFunction = (ctx: ScadaSymbolContext, element: Element, event: Event) => void; +export interface ScadaSymbolAction { + actionFunction?: string; + action?: ScadaSymbolActionFunction; +} + +export interface ScadaSymbolTag { + tag: string; + stateRenderFunction?: string; + stateRender?: ScadaSymbolTagStateRenderFunction; + actions?: {[trigger: string]: ScadaSymbolAction}; +} + +export enum ScadaSymbolBehaviorType { + value = 'value', + action = 'action', + widgetAction = 'widgetAction' +} + +export const scadaSymbolBehaviorTypes = Object.keys(ScadaSymbolBehaviorType) as ScadaSymbolBehaviorType[]; + +export const scadaSymbolBehaviorTypeTranslations = new Map( + [ + [ScadaSymbolBehaviorType.value, 'scada.behavior.type-value'], + [ScadaSymbolBehaviorType.action, 'scada.behavior.type-action'], + [ScadaSymbolBehaviorType.widgetAction, 'scada.behavior.type-widget-action'] + ] +); + + +export interface ScadaSymbolBehaviorBase { + id: string; + name: string; + hint?: string; + group?: string; + type: ScadaSymbolBehaviorType; +} + +export interface ScadaSymbolBehaviorValue extends ScadaSymbolBehaviorBase { + valueType: ValueType; + defaultGetValueSettings?: GetValueSettings; + trueLabel?: string; + falseLabel?: string; + stateLabel?: string; +} + +export interface ScadaSymbolBehaviorAction extends ScadaSymbolBehaviorBase { + valueType: ValueType; + defaultSetValueSettings?: SetValueSettings; + defaultWidgetActionSettings?: WidgetAction; +} + +export type ScadaSymbolBehavior = ScadaSymbolBehaviorValue & ScadaSymbolBehaviorAction; + +export enum ScadaSymbolPropertyType { + text = 'text', + number = 'number', + switch = 'switch', + color = 'color', + color_settings = 'color_settings', + font = 'font', + units = 'units' +} + +export const scadaSymbolPropertyTypes = Object.keys(ScadaSymbolPropertyType) as ScadaSymbolPropertyType[]; + +export const scadaSymbolPropertyTypeTranslations = new Map( + [ + [ScadaSymbolPropertyType.text, 'scada.property.type-text'], + [ScadaSymbolPropertyType.number, 'scada.property.type-number'], + [ScadaSymbolPropertyType.switch, 'scada.property.type-switch'], + [ScadaSymbolPropertyType.color, 'scada.property.type-color'], + [ScadaSymbolPropertyType.color_settings, 'scada.property.type-color-settings'], + [ScadaSymbolPropertyType.font, 'scada.property.type-font'], + [ScadaSymbolPropertyType.units, 'scada.property.type-units'] + ] +); + +export const scadaSymbolPropertyRowClasses = + ['column', 'column-xs', 'column-lt-md', 'align-start', 'no-border', 'no-gap', 'no-padding', 'same-padding']; + +export const scadaSymbolPropertyFieldClasses = + ['medium-width', 'flex', 'flex-xs', 'flex-lt-md']; + +export interface ScadaSymbolPropertyBase { + id: string; + name: string; + type: ScadaSymbolPropertyType; + default: any; + required?: boolean; + subLabel?: string; + divider?: boolean; + fieldSuffix?: string; + disableOnProperty?: string; + rowClass?: string; + fieldClass?: string; +} + +export interface ScadaSymbolNumberProperty extends ScadaSymbolPropertyBase { + min?: number; + max?: number; + step?: number; +} + +export type ScadaSymbolProperty = ScadaSymbolPropertyBase & ScadaSymbolNumberProperty; + +export interface ScadaSymbolMetadata { + title: string; + description?: string; + searchTags?: string[]; + widgetSizeX: number; + widgetSizeY: number; + stateRenderFunction?: string; + stateRender?: ScadaSymbolStateRenderFunction; + tags: ScadaSymbolTag[]; + behavior: ScadaSymbolBehavior[]; + properties: ScadaSymbolProperty[]; +} + +export const emptyMetadata = (): ScadaSymbolMetadata => ({ + title: '', + widgetSizeX: 3, + widgetSizeY: 3, + tags: [], + behavior: [], + properties: [] +}); + +const svgPartsRegex = /()(.*)<\/svg>/gms; + +const tbNamespaceRegex = //gms; + +export const applyTbNamespaceToSvgContent = (svgContent: string): string => { + svgPartsRegex.lastIndex = 0; + let svgRootNode: string; + let innerSvg = ''; + let match = svgPartsRegex.exec(svgContent); + if (match != null) { + if (match.length > 1) { + svgRootNode = match[1]; + } + if (match.length > 2) { + innerSvg = match[2]; + } + } + if (!svgRootNode) { + throw new Error('Invalid SVG document.'); + } + tbNamespaceRegex.lastIndex = 0; + match = tbNamespaceRegex.exec(svgRootNode); + if (match === null || !match.length) { + svgRootNode = svgRootNode.slice(0, -1) + ' xmlns:tb="https://thingsboard.io/svg">'; + return `${svgRootNode}\n${innerSvg}\n`; + } + return svgContent; +}; + +export const parseScadaSymbolMetadataFromContent = (svgContent: string): ScadaSymbolMetadata => { + try { + svgContent = applyTbNamespaceToSvgContent(svgContent); + const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); + return parseScadaSymbolMetadataFromDom(svgDoc); + } catch (_e) { + return emptyMetadata(); + } +}; + +const parseScadaSymbolMetadataFromDom = (svgDoc: Document): ScadaSymbolMetadata => { + try { + const elements = svgDoc.getElementsByTagName('tb:metadata'); + if (elements.length) { + return JSON.parse(elements[0].textContent); + } else { + return emptyMetadata(); + } + } catch (_e) { + console.error(_e); + return emptyMetadata(); + } +}; + +export const updateScadaSymbolMetadataInContent = (svgContent: string, metadata: ScadaSymbolMetadata): string => { + svgContent = applyTbNamespaceToSvgContent(svgContent); + const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml'); + const parsererror = svgDoc.getElementsByTagName('parsererror'); + if (parsererror?.length) { + return parsererror[0].outerHTML; + } + updateScadaSymbolMetadataInDom(svgDoc, metadata); + return svgDoc.documentElement.outerHTML; +}; + +const updateScadaSymbolMetadataInDom = (svgDoc: Document, metadata: ScadaSymbolMetadata) => { + let metadataElement: Node; + const elements = svgDoc.getElementsByTagName('tb:metadata'); + if (elements?.length) { + metadataElement = elements[0]; + metadataElement.textContent = ''; + } else { + metadataElement = svgDoc.createElement('tb:metadata'); + svgDoc.documentElement.insertBefore(metadataElement, svgDoc.documentElement.firstChild); + } + const content = JSON.stringify(metadata, null, 2); + const cdata = svgDoc.createCDATASection(content); + metadataElement.appendChild(cdata); +}; + +const tbMetadataRegex = /.*<\/tb:metadata>/gs; + +export interface ScadaSymbolContentData { + svgRootNode: string; + innerSvg: string; +} + +export const scadaSymbolContentData = (svgContent: string): ScadaSymbolContentData => { + const result: ScadaSymbolContentData = { + svgRootNode: '', + innerSvg: '' + }; + svgPartsRegex.lastIndex = 0; + const match = svgPartsRegex.exec(svgContent); + if (match != null) { + if (match.length > 1) { + result.svgRootNode = match[1]; + } + if (match.length > 2) { + let innerSvgContent = match[2]; + tbMetadataRegex.lastIndex = 0; + const metadataMatch = tbMetadataRegex.exec(svgContent); + if (metadataMatch !== null && metadataMatch.length) { + const metadata = metadataMatch[0]; + innerSvgContent = innerSvgContent.replace(metadata, ''); + } + result.innerSvg = innerSvgContent; + } + } + return result; +}; + +const defaultValueForValueType = (valueType: ValueType): any => { + if (!valueType) { + return null; + } + switch (valueType) { + case ValueType.STRING: + return ''; + case ValueType.INTEGER: + case ValueType.DOUBLE: + return 0; + case ValueType.BOOLEAN: + return false; + case ValueType.JSON: + return {}; + } +}; + +export const defaultGetValueSettings = (valueType: ValueType): GetValueSettings => ({ + action: GetValueAction.DO_NOTHING, + defaultValue: defaultValueForValueType(valueType), + executeRpc: { + method: 'getState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } +}); + +export const defaultSetValueSettings = (valueType: ValueType): SetValueSettings => ({ + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SERVER_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: valueType !== ValueType.BOOLEAN ? ValueToDataType.VALUE : ValueToDataType.CONSTANT, + constantValue: false, + valueToDataFunction: + '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' + } +}); + +export const defaultWidgetActionSettings: WidgetAction = { + type: WidgetActionType.doNothing, + targetDashboardStateId: null, + openRightLayout: false, + setEntityId: false, + stateEntityParamName: null +}; + +export const updateBehaviorDefaultSettings = (behavior: ScadaSymbolBehavior): ScadaSymbolBehavior => { + if (behavior.type) { + switch (behavior.type) { + case ScadaSymbolBehaviorType.value: + delete behavior.defaultSetValueSettings; + delete behavior.defaultWidgetActionSettings; + if (!behavior.defaultGetValueSettings) { + behavior.defaultGetValueSettings = mergeDeep({} as GetValueSettings, defaultGetValueSettings(behavior.valueType)); + } + break; + case ScadaSymbolBehaviorType.action: + delete behavior.defaultGetValueSettings; + delete behavior.defaultWidgetActionSettings; + if (!behavior.defaultSetValueSettings) { + behavior.defaultSetValueSettings = mergeDeep({} as SetValueSettings, defaultSetValueSettings(behavior.valueType)); + } + break; + case ScadaSymbolBehaviorType.widgetAction: + delete behavior.defaultGetValueSettings; + delete behavior.defaultSetValueSettings; + if (!behavior.defaultWidgetActionSettings) { + behavior.defaultWidgetActionSettings = mergeDeep({} as WidgetAction, defaultWidgetActionSettings); + } + break; + } + } + return behavior; +}; + +export const defaultScadaSymbolObjectSettings = (metadata: ScadaSymbolMetadata): ScadaSymbolObjectSettings => { + const settings: ScadaSymbolObjectSettings = { + behavior: {}, + properties: {} + }; + for (const behavior of metadata.behavior) { + if (behavior.type === ScadaSymbolBehaviorType.value) { + settings.behavior[behavior.id] = + mergeDeep({} as GetValueSettings, + defaultGetValueSettings(behavior.valueType), (behavior as ScadaSymbolBehaviorValue).defaultGetValueSettings || {}); + } else if (behavior.type === ScadaSymbolBehaviorType.action) { + settings.behavior[behavior.id] = + mergeDeep({} as SetValueSettings, + defaultSetValueSettings(behavior.valueType), (behavior as ScadaSymbolBehaviorAction).defaultSetValueSettings || {}); + } else if (behavior.type === ScadaSymbolBehaviorType.widgetAction) { + settings.behavior[behavior.id] = + mergeDeep({} as WidgetAction, + defaultWidgetActionSettings, (behavior as ScadaSymbolBehaviorAction).defaultWidgetActionSettings || {}); + } + } + for (const property of metadata.properties) { + settings.properties[property.id] = property.default; + } + return settings; +}; + +export type ScadaSymbolObjectSettings = { + behavior: {[id: string]: any}; + properties: {[id: string]: any}; +}; + +const parseError = (ctx: WidgetContext, err: any): string => + ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; + +export interface ScadaSymbolObjectCallbacks { + onScadaSymbolObjectLoadingState: (loading: boolean) => void; + onScadaSymbolObjectError: (error: string) => void; + onScadaSymbolObjectMessage: (message: string) => void; +} + +export class ScadaSymbolObject { + + private metadata: ScadaSymbolMetadata; + private settings: ScadaSymbolObjectSettings; + private context: ScadaSymbolContext; + + private svgShape: Svg; + private box: Box; + + private valueGetters: ValueGetter[] = []; + private valueActions: ValueAction[] = []; + private valueSetters: {[behaviorId: string]: ValueSetter} = {}; + + private stateValueSubjects: {[id: string]: BehaviorSubject} = {}; + + private readonly shapeResize$: ResizeObserver; + private readonly destroy$ = new Subject(); + + private scale = 1; + + private performInit = true; + + constructor(private rootElement: HTMLElement, + private ctx: WidgetContext, + private svgContent: string, + private inputSettings: ScadaSymbolObjectSettings, + private callbacks: ScadaSymbolObjectCallbacks, + private simulated: boolean) { + this.shapeResize$ = new ResizeObserver(() => { + this.resize(); + }); + this.svgContent = applyTbNamespaceToSvgContent(this.svgContent); + const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml'); + this.metadata = parseScadaSymbolMetadataFromDom(doc); + const defaults = defaultScadaSymbolObjectSettings(this.metadata); + this.settings = mergeDeep({} as ScadaSymbolObjectSettings, + defaults, this.inputSettings || {} as ScadaSymbolObjectSettings); + this.prepareMetadata(); + this.prepareSvgShape(doc); + this.shapeResize$.observe(this.rootElement); + } + + public destroy() { + this.destroy$.next(); + this.destroy$.complete(); + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + for (const stateValueId of Object.keys(this.stateValueSubjects)) { + this.stateValueSubjects[stateValueId].complete(); + this.stateValueSubjects[stateValueId].unsubscribe(); + } + this.valueActions.forEach(v => v.destroy()); + if (this.context) { + for (const tag of this.metadata.tags) { + const elements = this.context.tags[tag.tag]; + elements.forEach(element => { + element.timeline().stop(); + element.timeline(null); + }); + } + } + if (this.svgShape) { + this.svgShape.remove(); + } + } + + private prepareMetadata() { + this.metadata.stateRender = parseFunction(this.metadata.stateRenderFunction, ['ctx', 'svg']) || (() => {}); + for (const tag of this.metadata.tags) { + tag.stateRender = parseFunction(tag.stateRenderFunction, ['ctx', 'element']) || (() => {}); + if (tag.actions) { + for (const trigger of Object.keys(tag.actions)) { + const action = tag.actions[trigger]; + action.action = parseFunction(action.actionFunction, ['ctx', 'element', 'event']) || (() => {}); + } + } + } + } + + private prepareSvgShape(doc: XMLDocument) { + const elements = doc.getElementsByTagName('tb:metadata'); + for (let i=0;i { + const id = guid(); + const firstChar = id.charAt(0); + if (firstChar >= '0' && firstChar <= '9') { + return 'a' + id; + } else { + return id; + } + }, + formatValue, + text: this.setElementText.bind(this), + font: this.setElementFont.bind(this), + animate: this.animate.bind(this), + resetAnimation: this.resetAnimation.bind(this), + finishAnimation: this.finishAnimation.bind(this), + disable: this.disableElement.bind(this), + enable: this.enableElement.bind(this), + callAction: this.callAction.bind(this), + setValue: this.setValue.bind(this) + }, + tags: {}, + properties: {}, + values: {}, + svg: this.svgShape + }; + const taggedElements = this.svgShape.find(`[tb\\:tag]`); + for (const element of taggedElements) { + const tag: string = element.attr('tb:tag'); + let elements = this.context.tags[tag]; + if (!elements) { + elements = []; + this.context.tags[tag] = elements; + } + elements.push(element); + } + for (const property of this.metadata.properties) { + this.context.properties[property.id] = this.getPropertyValue(property.id); + } + for (const tag of this.metadata.tags) { + if (tag.actions) { + const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); + for (const trigger of Object.keys(tag.actions)) { + const action = tag.actions[trigger]; + elements.forEach(element => { + element.attr('cursor', 'pointer'); + element.on(trigger, (event) => { + action.action(this.context, element, event); + }); + }); + } + } + } + for (const behavior of this.metadata.behavior) { + if (behavior.type === ScadaSymbolBehaviorType.value) { + const getBehavior = behavior as ScadaSymbolBehaviorValue; + let getValueSettings: GetValueSettings = this.settings.behavior[getBehavior.id]; + getValueSettings = {...getValueSettings, actionLabel: + this.ctx.utilsService.customTranslation(getBehavior.name, getBehavior.name)}; + const stateValueSubject = new BehaviorSubject(getValueSettings.defaultValue); + this.stateValueSubjects[getBehavior.id] = stateValueSubject; + this.context.values[getBehavior.id] = getValueSettings.defaultValue; + stateValueSubject.pipe(takeUntil(this.destroy$)).subscribe((value) => { + this.onStateValueChanged(getBehavior.id, value); + }); + const valueGetter = + ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, { + next: (val) => {this.onValue(getBehavior.id, val);}, + error: (err) => { + const message = parseError(this.ctx, err); + this.onError(message); + } + }, this.simulated); + this.valueGetters.push(valueGetter); + this.valueActions.push(valueGetter); + } else if (behavior.type === ScadaSymbolBehaviorType.action) { + const setBehavior = behavior as ScadaSymbolBehaviorAction; + let setValueSettings: SetValueSettings = this.settings.behavior[setBehavior.id]; + setValueSettings = {...setValueSettings, actionLabel: + this.ctx.utilsService.customTranslation(setBehavior.name, setBehavior.name)}; + const valueSetter = ValueSetter.fromSettings(this.ctx, setValueSettings, this.simulated); + this.valueSetters[setBehavior.id] = valueSetter; + this.valueActions.push(valueSetter); + } else if (behavior.type === ScadaSymbolBehaviorType.widgetAction) { + // TODO: + } + } + this.renderState(); + if (this.valueGetters.length) { + const getValueObservables: Array> = []; + this.valueGetters.forEach(valueGetter => { + getValueObservables.push(valueGetter.getValue()); + }); + this.onLoadingState(true); + forkJoin(getValueObservables).pipe(takeUntil(this.destroy$)).subscribe( + { + next: () => { + this.onLoadingState(false); + }, + error: () => { + this.onLoadingState(false); + } + } + ); + } + } + + private onLoadingState(loading: boolean) { + this.callbacks.onScadaSymbolObjectLoadingState(loading); + } + + private onError(error: string) { + this.callbacks.onScadaSymbolObjectError(error); + } + + private onMessage(message: string) { + this.callbacks.onScadaSymbolObjectMessage(message); + } + + private callAction(event: Event, behaviorId: string, value?: any, observer?: Partial>) { + const behavior = this.metadata.behavior.find(b => b.id === behaviorId); + if (behavior) { + if (behavior.type === ScadaSymbolBehaviorType.action) { + const valueSetter = this.valueSetters[behaviorId]; + if (valueSetter) { + this.onLoadingState(true); + valueSetter.setValue(value).pipe(takeUntil(this.destroy$)).subscribe( + { + next: () => { + if (observer?.next) { + observer.next(); + } + this.onLoadingState(false); + }, + error: (err) => { + this.onLoadingState(false); + if (observer?.error) { + observer.error(err); + } + const message = parseError(this.ctx, err); + this.onError(message); + } + } + ); + } + } else if (behavior.type === ScadaSymbolBehaviorType.widgetAction) { + const widgetAction: WidgetAction = this.settings.behavior[behavior.id]; + if (this.simulated) { + const translatedType = this.ctx.translate.instant(widgetActionTypeTranslationMap.get(widgetAction.type)); + const message = this.ctx.translate.instant('scada.preview-widget-action-text', {type: translatedType}); + this.onMessage(message); + } else { + this.ctx.actionsApi.onWidgetAction(event, widgetAction); + } + if (observer?.next) { + observer.next(); + } + } + } + } + + private resize() { + if (this.svgShape) { + const targetWidth = this.rootElement.getBoundingClientRect().width; + const targetHeight = this.rootElement.getBoundingClientRect().height; + if (targetWidth && targetHeight) { + const svgAspect = this.box.width / this.box.height; + const shapeAspect = targetWidth / targetHeight; + let scale: number; + if (svgAspect > shapeAspect) { + scale = targetWidth / this.box.width; + } else { + scale = targetHeight / this.box.height; + } + if (this.scale !== scale) { + this.scale = scale; + this.svgShape.node.style.transform = `scale(${this.scale})`; + } + if (this.performInit) { + this.performInit = false; + this.init(); + } + } + } + } + + private onValue(id: string, value: any) { + const valueBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaSymbolBehaviorValue; + value = this.normalizeValue(value, valueBehavior.valueType); + this.setValue(valueBehavior.id, value); + } + + private setValue(valueId: string, value: any) { + const stateValueSubject = this.stateValueSubjects[valueId]; + if (stateValueSubject && stateValueSubject.value !== value) { + stateValueSubject.next(value); + } + } + + private onStateValueChanged(id: string, value: any) { + if (this.context.values[id] !== value) { + this.context.values[id] = value; + this.renderState(); + } + } + + private renderState(): void { + try { + this.metadata.stateRender(this.context, this.svgShape); + } catch (e) { + console.error(e); + } + for (const tag of this.metadata.tags) { + const elements = this.context.tags[tag.tag]; + if (elements) { + elements.forEach(element => { + try { + tag.stateRender(this.context, element); + } catch (e) { + console.error(e); + } + }); + } + } + } + + private normalizeValue(value: any, type: ValueType): any { + if (isUndefinedOrNull(value)) { + return defaultValueForValueType(type); + } else { + return value; + } + } + + private setElementText(e: Element | Element[], text: string) { + this.elements(e).forEach(element => { + let textElement: Text; + if (element.type === 'text') { + const children = element.children(); + if (children.length && children[0].type === 'tspan') { + textElement = children[0] as Text; + } else { + textElement = element as Text; + } + } else if (element.type === 'tspan') { + textElement = element as Text; + } + if (textElement) { + textElement.text(text); + } + }); + } + + private setElementFont(e: Element | Element[], font: Font, color: string) { + this.elements(e).forEach(element => { + if (element.type === 'text') { + const textElement = element as Text; + if (font) { + textElement.font({ + family: font.family, + size: (isDefinedAndNotNull(font.size) && isDefinedAndNotNull(font.sizeUnit)) ? + font.size + font.sizeUnit : null, + weight: font.weight, + style: font.style + }); + } + if (color) { + textElement.fill(color); + } + } + }); + } + + private animate(element: Element, duration: number): Runner { + this.finishAnimation(element); + return element.animate(duration, 0, 'now'); + } + + private resetAnimation(element: Element) { + element.timeline().stop(); + element.timeline(new Timeline()); + } + + private finishAnimation(element: Element) { + element.timeline().finish(); + element.timeline(new Timeline()); + } + + private disableElement(e: Element | Element[]) { + this.elements(e).forEach(element => { + element.attr({'pointer-events': 'none'}); + }); + } + + private enableElement(e: Element | Element[]) { + this.elements(e).forEach(element => { + element.attr({'pointer-events': null}); + }); + } + + private elements(element: Element | Element[]): Element[] { + return Array.isArray(element) ? element : [element]; + } + + private getProperty(id: string): ScadaSymbolProperty { + return this.metadata.properties.find(p => p.id === id); + } + + private getPropertyValue(id: string): any { + const property = this.getProperty(id); + if (property) { + const value = this.settings.properties[id]; + if (isDefinedAndNotNull(value)) { + if (property.type === ScadaSymbolPropertyType.color_settings) { + return ColorProcessor.fromSettings(value); + } else if (property.type === ScadaSymbolPropertyType.text) { + const result = this.ctx.utilsService.customTranslation(value, value); + const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo(); + return createLabelFromSubscriptionEntityInfo(entityInfo, result); + } + return value; + } else { + switch (property.type) { + case ScadaSymbolPropertyType.text: + return ''; + case ScadaSymbolPropertyType.number: + return 0; + case ScadaSymbolPropertyType.color: + return '#000'; + case ScadaSymbolPropertyType.color_settings: + return ColorProcessor.fromSettings(constantColor('#000')); + } + } + } else { + return ''; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html index 265a9c2501..b5c7c63650 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html @@ -21,9 +21,9 @@
widgets.button-state.activated-state
widgets.button-state.disabled-state
widgets.command-button.on-click
widgets.button-state.disabled-state
widgets.rpc-state.initial-state
widgets.power-button.power-on
widgets.power-button.power-off
widgets.rpc-state.disabled-state
widgets.rpc-state.initial-state
widgets.toggle-button.check
widgets.toggle-button.uncheck
widgets.rpc-state.disabled-state
div { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html index edb3651d38..53e6fb3e8a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html @@ -16,7 +16,7 @@ -->
-
{{ panelTitle | translate }}
+
{{ panelTitle }}
{{ 'widgets.value-action.action' | translate }}
@@ -122,7 +122,7 @@ helpId="widget/lib/rpc/parse_value_fn">
-
{{ 'widgets.value-action.state-when-result-is' | translate:{state: (stateLabel | translate)} }}
+
{{ 'widgets.value-action.state-when-result-is' | translate:{state: stateLabel} }}
-
{{ panelTitle | translate }}
+
{{ panelTitle }}
{{ 'widgets.value-action.action' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html index c5b4b50272..3fffd3f424 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html @@ -16,7 +16,7 @@ -->
-
{{ panelTitle | translate }}
+
{{ panelTitle }}
+ +
+
scada.behavior.behavior
+ +
+ + + +
{{ behaviorGroup.title | customTranslate }}
+
+
+ + + +
+
+ + + +
+
+
+
widget-config.appearance
+
+
+ + {{ propertyRow.label | customTranslate }} + +
{{ propertyRow.label | customTranslate }}
+
+ +
{{ property.subLabel | customTranslate }}
+ + +
{{ property.fieldSuffix | customTranslate }}
+
+ + + + + + +
{{ property.fieldSuffix | customTranslate }}
+
+ + + + +
+
+
+
+
+ + + + + + + + + +
+
{{ behavior.name | customTranslate }}
+ + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.scss new file mode 100644 index 0000000000..7f67f6ca37 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.scss @@ -0,0 +1,22 @@ +/** + * 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. + */ +:host ::ng-deep { + .tb-scada-symbol-appearance-properties { + gap: 16px; + display: flex; + flex-direction: column; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts new file mode 100644 index 0000000000..e68788cc92 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts @@ -0,0 +1,282 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + defaultScadaSymbolObjectSettings, + parseScadaSymbolMetadataFromContent, + ScadaSymbolBehaviorType, + ScadaSymbolMetadata, + ScadaSymbolObjectSettings, + ScadaSymbolPropertyType +} from '@home/components/widget/lib/scada/scada-symbol.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { TargetDevice, widgetType } from '@shared/models/widget.models'; +import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; +import { + ScadaSymbolBehaviorGroup, + ScadaSymbolPropertyRow, + toBehaviorGroups, + toPropertyRows +} from '@home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models'; +import { merge, Observable, of, Subscription } from 'rxjs'; +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; +import { ImageService } from '@core/http/image.service'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'tb-scada-symbol-object-settings', + templateUrl: './scada-symbol-object-settings.component.html', + styleUrls: ['./scada-symbol-object-settings.component.scss', './../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolObjectSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolObjectSettingsComponent), + multi: true + } + ] +}) +export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { + + ScadaSymbolBehaviorType = ScadaSymbolBehaviorType; + + ScadaSymbolPropertyType = ScadaSymbolPropertyType; + + @Input() + disabled: boolean; + + @Input() + scadaSymbolUrl: string; + + @Input() + scadaSymbolContent: string; + + @Input() + scadaSymbolMetadata: ScadaSymbolMetadata; + + @Input() + aliasController: IAliasController; + + @Input() + targetDevice: TargetDevice; + + @Input() + callbacks: WidgetActionCallbacks; + + @Input() + widgetType: widgetType; + + private modelValue: ScadaSymbolObjectSettings; + + private propagateChange = null; + + private validatorTriggers: string[]; + private validatorSubscription: Subscription; + + public scadaSymbolObjectSettingsFormGroup: UntypedFormGroup; + + metadata: ScadaSymbolMetadata; + behaviorGroups: ScadaSymbolBehaviorGroup[]; + propertyRows: ScadaSymbolPropertyRow[]; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + private imageService: ImageService, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.scadaSymbolObjectSettingsFormGroup = this.fb.group({ + behavior: this.fb.group({}), + properties: this.fb.group({}) + }); + this.scadaSymbolObjectSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.loadMetadata(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['scadaSymbolUrl', 'scadaSymbolContent', 'scadaSymbolMetadata'].includes(propName)) { + this.loadMetadata(); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.scadaSymbolObjectSettingsFormGroup.disable({emitEvent: false}); + } else { + this.scadaSymbolObjectSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: ScadaSymbolObjectSettings): void { + this.modelValue = value || { behavior: {}, properties: {} }; + this.setupValue(); + } + + validate(_c: UntypedFormControl) { + const valid = this.scadaSymbolObjectSettingsFormGroup.valid; + return valid ? null : { + scadaSymbolObjectSettings: { + valid: false, + }, + }; + } + + private loadMetadata() { + if (this.validatorSubscription) { + this.validatorSubscription.unsubscribe(); + this.validatorSubscription = null; + } + this.validatorTriggers = []; + + let metadata$: Observable; + if (this.scadaSymbolMetadata) { + metadata$ = of(this.scadaSymbolMetadata); + } else { + let content$: Observable; + if (this.scadaSymbolContent) { + content$ = of(this.scadaSymbolContent); + } else if (this.scadaSymbolUrl) { + content$ = this.imageService.getImageString(this.scadaSymbolUrl); + } else { + content$ = of(''); + } + metadata$ = content$.pipe( + map(content => parseScadaSymbolMetadataFromContent(content)) + ); + } + metadata$.subscribe( + (metadata) => { + this.metadata = metadata; + this.behaviorGroups = toBehaviorGroups(this.metadata.behavior); + this.propertyRows = toPropertyRows(this.metadata.properties); + const behaviorFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('behavior') as UntypedFormGroup; + for (const control of Object.keys(behaviorFormGroup.controls)) { + behaviorFormGroup.removeControl(control, {emitEvent: false}); + } + const propertiesFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('properties') as UntypedFormGroup; + for (const control of Object.keys(propertiesFormGroup.controls)) { + propertiesFormGroup.removeControl(control, {emitEvent: false}); + } + for (const behavior of this.metadata.behavior) { + behaviorFormGroup.addControl(behavior.id, this.fb.control(null, []), {emitEvent: false}); + } + for (const property of this.metadata.properties) { + if (property.disableOnProperty) { + if (!this.validatorTriggers.includes(property.disableOnProperty)) { + this.validatorTriggers.push(property.disableOnProperty); + } + } + const validators: ValidatorFn[] = []; + if (property.required) { + validators.push(Validators.required); + } + if (property.type === ScadaSymbolPropertyType.number) { + if (isDefinedAndNotNull(property.min)) { + validators.push(Validators.min(property.min)); + } + if (isDefinedAndNotNull(property.max)) { + validators.push(Validators.max(property.max)); + } + } + propertiesFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false}); + } + if (this.validatorTriggers.length) { + const observables: Observable[] = []; + for (const trigger of this.validatorTriggers) { + if (propertiesFormGroup.get(trigger)) { + observables.push(propertiesFormGroup.get(trigger).valueChanges); + } + } + if (observables.length) { + this.validatorSubscription = merge(...observables).subscribe(() => { + this.updateValidators(); + }); + } + } + this.setupValue(); + this.cd.markForCheck(); + } + ); + } + + private updateValidators() { + const propertiesFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('properties') as UntypedFormGroup; + for (const trigger of this.validatorTriggers) { + const value: boolean = propertiesFormGroup.get(trigger).value; + this.metadata.properties.filter(p => p.disableOnProperty === trigger).forEach( + (p) => { + const control = propertiesFormGroup.get(p.id); + if (value) { + control.enable({emitEvent: false}); + } else { + control.disable({emitEvent: false}); + } + } + ); + } + } + + private setupValue() { + if (this.metadata) { + const defaults = defaultScadaSymbolObjectSettings(this.metadata); + this.modelValue = mergeDeep(defaults, this.modelValue); + this.scadaSymbolObjectSettingsFormGroup.patchValue( + this.modelValue, {emitEvent: false} + ); + this.setDisabledState(this.disabled); + } + } + + private updateModel() { + this.modelValue = this.scadaSymbolObjectSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts new file mode 100644 index 0000000000..b884166cd2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts @@ -0,0 +1,77 @@ +/// +/// 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 { + ScadaSymbolBehavior, + ScadaSymbolProperty, + ScadaSymbolPropertyType +} from '@home/components/widget/lib/scada/scada-symbol.models'; + +export interface ScadaSymbolBehaviorGroup { + title?: string; + behaviors: ScadaSymbolBehavior[]; +} + +export interface ScadaSymbolPropertyRow { + label: string; + properties: ScadaSymbolProperty[]; + switch?: ScadaSymbolProperty; + rowClass?: string; +} + +export const toBehaviorGroups = (behaviors: ScadaSymbolBehavior[]): ScadaSymbolBehaviorGroup[] => { + const result: ScadaSymbolBehaviorGroup[] = []; + for (const behavior of behaviors) { + if (!behavior.group) { + result.push({ + title: null, + behaviors: [behavior] + }); + } else { + let behaviorGroup = result.find(g => g.title === behavior.group); + if (!behaviorGroup) { + behaviorGroup = { + title: behavior.group, + behaviors: [] + }; + result.push(behaviorGroup); + } + behaviorGroup.behaviors.push(behavior); + } + } + return result; +}; + +export const toPropertyRows = (properties: ScadaSymbolProperty[]): ScadaSymbolPropertyRow[] => { + const result: ScadaSymbolPropertyRow[] = []; + for (const property of properties) { + let propertyRow = result.find(r => r.label === property.name); + if (!propertyRow) { + propertyRow = { + label: property.name, + properties: [], + rowClass: property.rowClass + }; + result.push(propertyRow); + } + if (property.type === ScadaSymbolPropertyType.switch) { + propertyRow.switch = property; + } else { + propertyRow.properties.push(property); + } + } + return result; +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 781042a4cf..fbfffc973f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -155,6 +155,9 @@ import { GradientComponent } from '@home/components/widget/lib/settings/common/g import { ValueSourceDataKeyComponent } from '@home/components/widget/lib/settings/common/value-source-data-key.component'; +import { + ScadaSymbolObjectSettingsComponent +} from '@home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component'; @NgModule({ declarations: [ @@ -211,6 +214,7 @@ import { TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, + ScadaSymbolObjectSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent, AdvancedRangeComponent, @@ -275,6 +279,7 @@ import { TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, + ScadaSymbolObjectSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent, AdvancedRangeComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html index 9208efd68a..db6d0371f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html @@ -21,11 +21,11 @@
widgets.rpc-state.initial-state
widgets.rpc-state.turn-on
widgets.rpc-state.turn-off
widgets.rpc-state.disabled-state
widgets.slider.initial-value
widgets.slider.on-value-change
widgets.rpc-state.disabled-state
widgets.rpc-state.initial-state
widgets.rpc-state.disabled-state
+ +
+
scada.symbol.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 778e84a614..37f61b11fb 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 @@ -365,6 +365,9 @@ import { import { UnreadNotificationWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/unread-notification-widget-settings.component'; +import { +ScadaSymbolWidgetSettingsComponent +} from '@home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component'; @NgModule({ declarations: [ @@ -495,6 +498,7 @@ import { LabelCardWidgetSettingsComponent, LabelValueCardWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent, + ScadaSymbolWidgetSettingsComponent ], imports: [ CommonModule, @@ -629,7 +633,8 @@ import { RadarChartWidgetSettingsComponent, LabelCardWidgetSettingsComponent, LabelValueCardWidgetSettingsComponent, - UnreadNotificationWidgetSettingsComponent + UnreadNotificationWidgetSettingsComponent, + ScadaSymbolWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -731,5 +736,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type o.value === this.selectedOption)) { this.selectedOption = this.headerOptions[0].value; } @@ -962,7 +968,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe if (this.modelValue) { const config = this.modelValue.config; if (this.widgetType === widgetType.rpc && this.modelValue.isDataEnabled) { - if (!this.widgetEditMode && !targetDeviceValid(config.targetDevice)) { + if ((!this.widgetEditMode && !this.modelValue?.typeParameters.targetDeviceOptional) && !targetDeviceValid(config.targetDevice)) { return { targetDevice: { valid: false diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index 176e305d94..43223de4b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -26,12 +26,10 @@ 'mat-elevation-z4': widget.dropShadow, 'tb-overflow-visible': widgetComponent.widgetContext?.overflowVisible, 'tb-has-timewindow': widget.hasTimewindow, - 'tb-edit': isEdit + 'tb-edit': isEdit || isEditingWidget, + 'tb-hover': hovered }" - [ngStyle]="widget.style" - (mousedown)="onMouseDown($event)" - (click)="onClicked($event)" - (contextmenu)="onContextMenu($event)"> + [ngStyle]="widget.style">
@@ -57,32 +55,11 @@ - - -
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss index 3143ab1ee2..7b6d8e59f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss @@ -102,6 +102,12 @@ div.tb-widget { padding: 0 !important; margin: 0 !important; line-height: 20px; + &.mat-mdc-button-base { + .mat-mdc-button-touch-target { + height: 32px; + width: 32px; + } + } .mat-icon { width: 20px; @@ -141,7 +147,69 @@ div.tb-widget { opacity: .5; } + &.tb-hover { + &:not(.tb-highlighted) { + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + } + } + &.tb-edit { cursor: pointer; } } + +gridster-item:hover { + .tb-widget-container { + div.tb-widget { + &.tb-edit { + &:not(.tb-highlighted) { + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + } + } + } + } +} + +.tooltipster-sidetip.tb-widget-edit-actions-tooltip { + .tooltipster-box { + user-select: none; + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.38); + box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); + .tooltipster-content { + padding: 4px 8px; + font-size: 12px; + line-height: 12px; + font-weight: 500; + color: rgba(0, 0, 0, 0.76); + .tb-widget-actions-panel { + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; + gap: 8px; + } + } + } + .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; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index 81c206391a..1546c4f92a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -22,12 +22,12 @@ import { ElementRef, EventEmitter, HostBinding, - Input, + Input, OnChanges, OnDestroy, OnInit, Output, - Renderer2, - ViewChild, + Renderer2, SimpleChanges, + ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; @@ -38,6 +38,8 @@ import { SafeStyle } from '@angular/platform-browser'; import { isNotEmptyStr } from '@core/utils'; import { GridsterItemComponent } from 'angular-gridster2'; import { UtilsService } from '@core/services/utils.service'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import { from } from 'rxjs'; export enum WidgetComponentActionType { MOUSE_DOWN, @@ -61,7 +63,7 @@ export class WidgetComponentAction { encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) -export class WidgetContainerComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { +export class WidgetContainerComponent extends PageComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy { @HostBinding('class') widgetContainerClass = 'tb-widget-container'; @@ -84,6 +86,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A @Input() isEdit: boolean; + @Input() + isEditingWidget: boolean; + @Input() isPreview: boolean; @@ -111,11 +116,20 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A @Output() widgetComponentAction: EventEmitter = new EventEmitter(); + hovered = false; + + get widgetEditActionsEnabled(): boolean { + return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen; + } + private cssClass: string; + private editWidgetActionsTooltip: ITooltipsterInstance; + constructor(protected store: Store, private cd: ChangeDetectorRef, private renderer: Renderer2, + private container: ViewContainerRef, private utils: UtilsService) { super(store); } @@ -127,16 +141,34 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A this.cssClass = this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString); } + $(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent)); + $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent)); + $(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent)); + this.initEditWidgetActionTooltip(); } ngAfterViewInit(): void { this.widget.widgetContext.$widgetElement = $(this.tbWidgetElement.nativeElement); } + ngOnChanges(changes: SimpleChanges) { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['isEditActionEnabled', 'isRemoveActionEnabled', 'isExportActionEnabled'].includes(propName)) { + this.updateEditWidgetActionsTooltipState(); + } + } + } + } + ngOnDestroy(): void { if (this.cssClass) { this.utils.clearCssElement(this.renderer, this.cssClass); } + if (this.editWidgetActionsTooltip) { + this.editWidgetActionsTooltip.destroy(); + } } isHighlighted(widget: DashboardWidget) { @@ -198,4 +230,141 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A }); } + updateEditWidgetActionsTooltipState() { + if (this.editWidgetActionsTooltip) { + if (this.widgetEditActionsEnabled) { + this.editWidgetActionsTooltip.enable(); + } else { + this.editWidgetActionsTooltip.disable(); + } + } + } + + private initEditWidgetActionTooltip() { + from(import('tooltipster')).subscribe(() => { + $(this.gridsterItem.el).tooltipster({ + delay: this.widget.selected ? [0, 10000000] : [0, 100], + distance: 2, + zIndex: 151, + arrow: false, + theme: ['tb-widget-edit-actions-tooltip'], + interactive: true, + trigger: 'custom', + triggerOpen: { + mouseenter: true + }, + triggerClose: { + mouseleave: true + }, + side: ['top'], + trackOrigin: true, + trackerInterval: 25, + content: '', + functionPosition: (instance, helper, position) => { + const clientRect = helper.origin.getBoundingClientRect(); + position.coord.left = clientRect.right - position.size.width; + position.target = clientRect.right; + return position; + }, + functionReady: (_instance, helper) => { + const tooltipEl = $(helper.tooltip); + tooltipEl.on('mouseenter', () => { + this.hovered = true; + this.cd.markForCheck(); + }); + tooltipEl.on('mouseleave', () => { + this.hovered = false; + this.cd.markForCheck(); + }); + }, + functionAfter: () => { + this.hovered = false; + this.cd.markForCheck(); + } + }); + this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance'); + const componentRef = this.container.createComponent(EditWidgetActionsTooltipComponent); + componentRef.instance.container = this; + componentRef.instance.viewInited.subscribe(() => { + if (this.editWidgetActionsTooltip.status().open) { + this.editWidgetActionsTooltip.reposition(); + } + }); + this.editWidgetActionsTooltip.on('destroyed', () => { + componentRef.destroy(); + }); + const parentElement = componentRef.instance.element.nativeElement; + const content = parentElement.firstChild; + parentElement.removeChild(content); + parentElement.style.display = 'none'; + this.editWidgetActionsTooltip.content(content); + this.updateEditWidgetActionsTooltipState(); + this.widget.onSelected((selected) => + this.updateEditWidgetActionsTooltipSelectedState(selected)); + }); + } + + private updateEditWidgetActionsTooltipSelectedState(selected: boolean) { + if (this.editWidgetActionsTooltip) { + if (selected) { + this.editWidgetActionsTooltip.option('delay', [0, 10000000]); + this.editWidgetActionsTooltip.option('triggerClose', { + mouseleave: false + }); + if (this.widgetEditActionsEnabled) { + this.editWidgetActionsTooltip.open(); + } + } else { + this.editWidgetActionsTooltip.option('delay', [0, 100]); + this.editWidgetActionsTooltip.option('triggerClose', { + mouseleave: true + }); + this.editWidgetActionsTooltip.close(); + } + } + } + +} + +@Component({ + template: `
+ + + +
`, + styles: [], + encapsulation: ViewEncapsulation.None +}) +export class EditWidgetActionsTooltipComponent implements AfterViewInit { + + @Input() + container: WidgetContainerComponent; + + @Output() + viewInited = new EventEmitter(); + + constructor(public element: ElementRef) { + } + + ngAfterViewInit() { + this.viewInited.emit(); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index bae0389e61..1941c4edb3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -39,7 +39,7 @@ import { } from '@angular/core'; import { DashboardWidget } from '@home/models/dashboard-component.models'; import { - Widget, + Widget, WidgetAction, WidgetActionDescriptor, widgetActionSources, WidgetActionType, @@ -57,7 +57,7 @@ import { WidgetService } from '@core/http/widget.service'; import { UtilsService } from '@core/services/utils.service'; import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; import { - deepClone, + deepClone, guid, insertVariable, isDefined, isNotEmptyStr, @@ -254,6 +254,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI actionDescriptorsBySourceId, getActionDescriptors: this.getActionDescriptors.bind(this), handleWidgetAction: this.handleWidgetAction.bind(this), + onWidgetAction: this.onWidgetAction.bind(this), elementClick: this.elementClick.bind(this), cardClick: this.cardClick.bind(this), click: this.click.bind(this), @@ -1040,7 +1041,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI return result; } - private handleWidgetAction($event: Event, descriptor: WidgetActionDescriptor, + private handleWidgetAction($event: Event, descriptor: WidgetAction, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string): void { const type = descriptor.type; const targetEntityParamName = descriptor.stateEntityParamName; @@ -1113,7 +1114,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI const customHtml = descriptor.customHtml; const customCss = descriptor.customCss; const customResources = descriptor.customResources; - const actionNamespace = `custom-action-pretty-${descriptor.name.toLowerCase()}`; + const actionNamespace = `custom-action-pretty-${guid()}`; let htmlTemplate = ''; if (isDefined(customHtml) && customHtml.length > 0) { htmlTemplate = customHtml; @@ -1421,13 +1422,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(','); const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]); if (targetElement.length && targetElement[0].id) { - $event.stopPropagation(); const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id); - const entityInfo = this.getActiveEntityInfo(); - const entityId = entityInfo ? entityInfo.entityId : null; - const entityName = entityInfo ? entityInfo.entityName : null; - const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); + this.onWidgetAction($event, descriptor); } } } @@ -1443,18 +1439,23 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI private onClick($event: Event, sourceId: string) { const descriptors = this.getActionDescriptors(sourceId); if (descriptors.length) { + this.onWidgetAction($event, descriptors[0]); + } + } + + private onWidgetAction($event: Event, action: WidgetAction) { + if ($event) { $event.stopPropagation(); - const descriptor = descriptors[0]; - const entityInfo = this.getActiveEntityInfo(); - const entityId = entityInfo ? entityInfo.entityId : null; - const entityName = entityInfo ? entityInfo.entityName : null; - const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); } + const entityInfo = this.getActiveEntityInfo(); + const entityId = entityInfo ? entityInfo.entityId : null; + const entityName = entityInfo ? entityInfo.entityName : null; + const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; + this.handleWidgetAction($event, action, entityId, entityName, null, entityLabel); } private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array, - actionDescriptor: WidgetActionDescriptor): Observable { + actionDescriptor: WidgetAction): Observable { const resourceTasks: Observable[] = []; const modulesTasks: Observable[] = []; diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index d74373f734..4595ad7942 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -68,6 +68,7 @@ export interface DashboardCallbacks { onExportWidget?: ($event: Event, widget: Widget, widgeTitle: string) => void; onRemoveWidget?: ($event: Event, widget: Widget) => void; onWidgetMouseDown?: ($event: Event, widget: Widget) => void; + onDashboardMouseDown?: ($event: Event) => void; onWidgetClicked?: ($event: Event, widget: Widget) => void; prepareDashboardContextMenu?: ($event: Event) => Array; prepareWidgetContextMenu?: ($event: Event, widget: Widget) => Array; @@ -240,11 +241,20 @@ export class DashboardWidgets implements Iterable { highlightWidget(widgetId: string): DashboardWidget { const widget = this.findWidgetById(widgetId); if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) { - this.highlightedMode = true; + let detectChanges = false; + if (!this.highlightedMode) { + this.highlightedMode = true; + detectChanges = true; + } widget.highlighted = true; + widget.selected = false; this.dashboardWidgets.forEach((dashboardWidget) => { if (dashboardWidget !== widget) { dashboardWidget.highlighted = false; + dashboardWidget.selected = false; + if (detectChanges) { + dashboardWidget.widgetContext?.detectContainerChanges(); + } } }); return widget; @@ -330,6 +340,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { private highlightedValue = false; private selectedValue = false; + private selectedCallback: (selected: boolean) => void = () => {}; isFullscreen = false; @@ -399,6 +410,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { } } + onSelected(selectedCallback: (selected: boolean) => void) { + this.selectedCallback = selectedCallback; + } + get selected() { return this.selectedValue; } @@ -406,6 +421,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { set selected(selected: boolean) { if (this.selectedValue !== selected) { this.selectedValue = selected; + this.selectedCallback(selected); this.widgetContext.detectContainerChanges(); } } diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index fbda78a128..1441bd4e92 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -533,6 +533,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri widgetName: string; fullFqn: string; deprecated: boolean; + scada: boolean; typeSettingsSchema?: string | any; typeDataKeySettingsSchema?: string | any; typeLatestDataKeySettingsSchema?: string | any; @@ -567,6 +568,7 @@ export const MissingWidgetType: WidgetInfo = { widgetName: 'Widget type not found', fullFqn: 'undefined', deprecated: false, + scada: false, sizeX: 8, sizeY: 6, resources: [], @@ -592,6 +594,7 @@ export const ErrorWidgetType: WidgetInfo = { widgetName: 'Error loading widget', fullFqn: 'error', deprecated: false, + scada: false, sizeX: 8, sizeY: 6, resources: [], @@ -634,6 +637,7 @@ export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({ widgetName: widgetTypeEntity.name, fullFqn: fullWidgetTypeFqn(widgetTypeEntity), deprecated: widgetTypeEntity.deprecated, + scada: widgetTypeEntity.scada, type: widgetTypeEntity.descriptor.type, sizeX: widgetTypeEntity.descriptor.sizeX, sizeY: widgetTypeEntity.descriptor.sizeY, @@ -687,6 +691,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: fqn: widgetTypeFqn(widgetInfo.fullFqn), name: widgetInfo.widgetName, deprecated: widgetInfo.deprecated, + scada: widgetInfo.scada, descriptor }; }; diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index f05fc05a25..1c4d6c204d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { Injectable, NgModule } from '@angular/core'; -import { Resolve, RouterModule, Routes } from '@angular/router'; +import { inject, Injectable, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router'; import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; @@ -23,7 +23,7 @@ import { Authority } from '@shared/models/authority.enum'; import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component'; -import { Observable } from 'rxjs'; +import { forkJoin, Observable } from 'rxjs'; import { OAuth2Service } from '@core/http/oauth2.service'; import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; @@ -31,7 +31,7 @@ import { EntitiesTableComponent } from '@home/components/entity/entities-table.c import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/resources-library-table-config.resolve'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; -import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; @@ -41,6 +41,10 @@ import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; +import { ImageResourceType, IMAGES_URL_PREFIX, ResourceSubType } from '@shared/models/resource.models'; +import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; +import { ImageService } from '@core/http/image.service'; +import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -53,6 +57,22 @@ export class OAuth2LoginProcessingUrlResolver implements Resolve { } } +export const scadaSymbolResolver: ResolveFn = + (route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + imageService = inject(ImageService)) => { + const type: ImageResourceType = route.params.type; + const key = decodeURIComponent(route.params.key); + return forkJoin({ + imageResource: imageService.getImageInfo(type, key), + scadaSymbolContent: imageService.getImageString(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`) + }); +}; + +export const scadaSymbolBreadcumbLabelFunction: BreadCrumbLabelFunction + = ((route, translate, component) => + component.symbolData?.imageResource?.title); + const routes: Routes = [ { path: 'resources', @@ -88,8 +108,45 @@ const routes: Routes = [ data: { auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], title: 'image.gallery', - }, + imageSubType: ResourceSubType.IMAGE + } + } + ] + }, + { + path: 'scada-symbols', + data: { + breadcrumb: { + label: 'scada.symbols', + icon: 'view_in_ar' } + }, + children: [ + { + path: '', + component: ImageGalleryComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'scada.symbols', + imageSubType: ResourceSubType.SCADA_SYMBOL + } + }, + { + path: ':type/:key', + component: ScadaSymbolComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: scadaSymbolBreadcumbLabelFunction, + icon: 'view_in_ar' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'scada.symbol.symbol' + }, + resolve: { + symbolData: scadaSymbolResolver + } + }, ] }, { diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 63a3aace21..0b1a70996e 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -43,6 +43,7 @@ import { EntitiesModule } from '@home/pages/entities/entities.module'; import { FeaturesModule } from '@home/pages/features/features.module'; import { NotificationModule } from '@home/pages/notification/notification.module'; import { AccountModule } from '@home/pages/account/account.module'; +import { ScadaSymbolModule } from '@home/pages/scada-symbol/scada-symbol.module'; @NgModule({ exports: [ @@ -72,7 +73,8 @@ import { AccountModule } from '@home/pages/account/account.module'; OtaUpdateModule, UserModule, VcModule, - AccountModule + AccountModule, + ScadaSymbolModule ] }) export class HomePagesModule { } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html new file mode 100644 index 0000000000..96d8788a38 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html @@ -0,0 +1,147 @@ + +
+
{{ panelTitle | translate }}
+
+
+
scada.behavior.id
+ + + +
+
+
scada.behavior.name
+ + + +
+
+
scada.behavior.hint
+ + + +
+
+
scada.behavior.group-title
+ + + +
+
+
scada.behavior.type
+ + + + {{ scadaSymbolBehaviorTypeTranslations.get(type) | translate }} + + + +
+
+
scada.behavior.value-type
+ + + +
+ + {{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }} +
+
+ + + {{ valueTypesMap.get(valueType).name | translate }} + +
+
+
+ + +
+
scada.behavior.true-label
+ + + +
+
+
scada.behavior.false-label
+ + + +
+
+
scada.behavior.state-label
+ + + +
+
+
+
scada.behavior.default-settings
+ + +
+
+ +
+
scada.behavior.default-settings
+ + +
+
+ +
+
scada.behavior.default-settings
+ + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss new file mode 100644 index 0000000000..3fbbd245a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss @@ -0,0 +1,67 @@ +/** + * 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 '../../../../../../scss/constants'; + +.tb-scada-symbol-behavior-settings-panel { + width: 540px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-scada-symbol-behavior-settings-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + .mat-mdc-form-field.tb-value-type { + .mat-mdc-form-field-infix { + max-height: 40px; + } + mat-select-trigger { + .tb-value-type-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 10px; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + vertical-align: bottom; + } + } + } + } + } + .tb-scada-symbol-behavior-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-scada-symbol-behavior-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts new file mode 100644 index 0000000000..f7ce9c7a0e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts @@ -0,0 +1,189 @@ +/// +/// 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, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { merge } from 'rxjs'; +import { + defaultGetValueSettings, defaultSetValueSettings, defaultWidgetActionSettings, + ScadaSymbolBehavior, + ScadaSymbolBehaviorType, + scadaSymbolBehaviorTypes, + scadaSymbolBehaviorTypeTranslations +} from '@app/modules/home/components/widget/lib/scada/scada-symbol.models'; +import { ValueType, valueTypesMap } from '@shared/models/constants'; +import { GetValueSettings, SetValueSettings, ValueToDataType } from '@shared/models/action-widget-settings.models'; +import { WidgetService } from '@core/http/widget.service'; +import { mergeDeep } from '@core/utils'; +import { WidgetAction, widgetType } from '@shared/models/widget.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; + +@Component({ + selector: 'tb-scada-symbol-behavior-panel', + templateUrl: './scada-symbol-behavior-panel.component.html', + styleUrls: ['./scada-symbol-behavior-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolBehaviorPanelComponent implements OnInit { + + widgetType = widgetType; + + ScadaSymbolBehaviorType = ScadaSymbolBehaviorType; + + ValueType = ValueType; + + ValueToDataType = ValueToDataType; + + scadaSymbolBehaviorTypes = scadaSymbolBehaviorTypes; + scadaSymbolBehaviorTypeTranslations = scadaSymbolBehaviorTypeTranslations; + + valueTypes = Object.keys(ValueType) as ValueType[]; + + valueTypesMap = valueTypesMap; + + @Input() + isAdd = false; + + @Input() + behavior: ScadaSymbolBehavior; + + @Input() + aliasController: IAliasController; + + @Input() + callbacks: WidgetActionCallbacks; + + @Input() + disabled: boolean; + + @Input() + popover: TbPopoverComponent; + + @Output() + behaviorSettingsApplied = new EventEmitter(); + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + panelTitle: string; + + behaviorFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService) { + } + + ngOnInit(): void { + this.panelTitle = this.isAdd ? 'scada.behavior.add-behavior' : 'scada.behavior.behavior-settings'; + this.behaviorFormGroup = this.fb.group( + { + id: [this.behavior.id, [Validators.required]], + name: [this.behavior.name, [Validators.required]], + hint: [this.behavior.hint, []], + group: [this.behavior.group, []], + type: [this.behavior.type, [Validators.required]], + valueType: [this.behavior.valueType, [Validators.required]], + trueLabel: [this.behavior.trueLabel, []], + falseLabel: [this.behavior.falseLabel, []], + stateLabel: [this.behavior.stateLabel, []], + defaultGetValueSettings: [this.behavior.defaultGetValueSettings, [Validators.required]], + defaultSetValueSettings: [this.behavior.defaultSetValueSettings, [Validators.required]], + defaultWidgetActionSettings: [this.behavior.defaultWidgetActionSettings, [Validators.required]] + } + ); + if (this.disabled) { + this.behaviorFormGroup.disable({emitEvent: false}); + } else { + merge(this.behaviorFormGroup.get('type').valueChanges, + this.behaviorFormGroup.get('valueType').valueChanges).subscribe(() => { + this.updateValidators(); + }); + this.updateValidators(); + } + } + + cancel() { + this.popover?.hide(); + } + + applyBehaviorSettings() { + const behavior = this.behaviorFormGroup.getRawValue(); + this.behaviorSettingsApplied.emit(behavior); + } + + private updateValidators() { + const type: ScadaSymbolBehaviorType = this.behaviorFormGroup.get('type').value; + const valueType: ValueType = this.behaviorFormGroup.get('valueType').value; + let defaultGetValueSettingsValue = this.behaviorFormGroup.get('defaultGetValueSettings').value; + let defaultSetValueSettingsValue = this.behaviorFormGroup.get('defaultSetValueSettings').value; + let defaultWidgetActionSettingsValue = this.behaviorFormGroup.get('defaultWidgetActionSettings').value; + this.behaviorFormGroup.disable({emitEvent: false}); + this.behaviorFormGroup.get('id').enable({emitEvent: false}); + this.behaviorFormGroup.get('name').enable({emitEvent: false}); + this.behaviorFormGroup.get('type').enable({emitEvent: false}); + this.behaviorFormGroup.get('hint').enable({emitEvent: false}); + this.behaviorFormGroup.get('group').enable({emitEvent: false}); + switch (type) { + case ScadaSymbolBehaviorType.value: + this.behaviorFormGroup.get('valueType').enable({emitEvent: false}); + this.behaviorFormGroup.get('defaultGetValueSettings').enable({emitEvent: false}); + if (valueType === ValueType.BOOLEAN) { + this.behaviorFormGroup.get('trueLabel').enable({emitEvent: false}); + this.behaviorFormGroup.get('falseLabel').enable({emitEvent: false}); + this.behaviorFormGroup.get('stateLabel').enable({emitEvent: false}); + } + if (!defaultGetValueSettingsValue) { + defaultGetValueSettingsValue = mergeDeep({} as GetValueSettings, defaultGetValueSettings(valueType)); + this.behaviorFormGroup.get('defaultGetValueSettings').patchValue(defaultGetValueSettingsValue, {emitEvent: true}); + } + if (defaultSetValueSettingsValue) { + this.behaviorFormGroup.get('defaultSetValueSettings').patchValue(null, {emitEvent: true}); + } + if (defaultWidgetActionSettingsValue) { + this.behaviorFormGroup.get('defaultWidgetActionSettings').patchValue(null, {emitEvent: true}); + } + break; + case ScadaSymbolBehaviorType.action: + this.behaviorFormGroup.get('valueType').enable({emitEvent: false}); + this.behaviorFormGroup.get('defaultSetValueSettings').enable({emitEvent: false}); + if (!defaultSetValueSettingsValue) { + defaultSetValueSettingsValue = mergeDeep({} as SetValueSettings, defaultSetValueSettings(valueType)); + this.behaviorFormGroup.get('defaultSetValueSettings').patchValue(defaultSetValueSettingsValue, {emitEvent: true}); + } + if (defaultGetValueSettingsValue) { + this.behaviorFormGroup.get('defaultGetValueSettings').patchValue(null, {emitEvent: true}); + } + if (defaultWidgetActionSettingsValue) { + this.behaviorFormGroup.get('defaultWidgetActionSettings').patchValue(null, {emitEvent: true}); + } + break; + case ScadaSymbolBehaviorType.widgetAction: + this.behaviorFormGroup.get('defaultWidgetActionSettings').enable({emitEvent: false}); + if (!defaultWidgetActionSettingsValue) { + defaultWidgetActionSettingsValue = mergeDeep({} as WidgetAction, defaultWidgetActionSettings); + this.behaviorFormGroup.get('defaultWidgetActionSettings').patchValue(defaultWidgetActionSettingsValue, {emitEvent: true}); + } + if (defaultGetValueSettingsValue) { + this.behaviorFormGroup.get('defaultGetValueSettings').patchValue(null, {emitEvent: true}); + } + if (defaultSetValueSettingsValue) { + this.behaviorFormGroup.get('defaultSetValueSettings').patchValue(null, {emitEvent: true}); + } + break; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html new file mode 100644 index 0000000000..004b02bbd0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html @@ -0,0 +1,50 @@ + + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.scss new file mode 100644 index 0000000000..d0592c431e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-metadata-behavior-row { + .tb-id-field { + flex: 1 1 40%; + } + .tb-name-field { + flex: 1 1 40%; + } + .tb-type-field { + flex: 1 1 20%; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts new file mode 100644 index 0000000000..eb71f4ff9f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts @@ -0,0 +1,299 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + Renderer2, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { + ScadaSymbolBehavior, + ScadaSymbolBehaviorType, + scadaSymbolBehaviorTypes, + scadaSymbolBehaviorTypeTranslations, updateBehaviorDefaultSettings +} from '@home/components/widget/lib/scada/scada-symbol.models'; +import { deepClone, isUndefinedOrNull } from '@core/utils'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { + ScadaSymbolBehaviorPanelComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component'; +import { ValueToDataType } from '@shared/models/action-widget-settings.models'; +import { + ScadaSymbolBehaviorsComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component'; +import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; + +export const behaviorValid = (behavior: ScadaSymbolBehavior): boolean => { + if (!behavior.id || !behavior.name || !behavior.type) { + return false; + } + switch (behavior.type) { + case ScadaSymbolBehaviorType.value: + if (!behavior.valueType || !behavior.defaultGetValueSettings) { + return false; + } + break; + case ScadaSymbolBehaviorType.action: + if (!behavior.defaultSetValueSettings) { + return false; + } + if (behavior.defaultSetValueSettings.valueToData?.type === ValueToDataType.CONSTANT + && isUndefinedOrNull(behavior.defaultSetValueSettings.valueToData?.constantValue)) { + return false; + } + if (behavior.defaultSetValueSettings.valueToData?.type === ValueToDataType.FUNCTION + && isUndefinedOrNull(behavior.defaultSetValueSettings.valueToData?.valueToDataFunction)) { + return false; + } + break; + case ScadaSymbolBehaviorType.widgetAction: + break; + } + return true; +}; + +@Component({ + selector: 'tb-scada-symbol-metadata-behavior-row', + templateUrl: './scada-symbol-behavior-row.component.html', + styleUrls: ['./scada-symbol-behavior-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, OnInit, Validator { + + @ViewChild('idInput') + idInput: ElementRef; + + @ViewChild('editButton') + editButton: MatButton; + + scadaSymbolBehaviorTypes = scadaSymbolBehaviorTypes; + scadaSymbolBehaviorTypeTranslations = scadaSymbolBehaviorTypeTranslations; + + @Input() + disabled: boolean; + + @Input() + index: number; + + @Input() + aliasController: IAliasController; + + @Input() + callbacks: WidgetActionCallbacks; + + @Output() + behaviorRemoved = new EventEmitter(); + + behaviorRowFormGroup: UntypedFormGroup; + + modelValue: ScadaSymbolBehavior; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private behaviorsComponent: ScadaSymbolBehaviorsComponent) { + } + + ngOnInit() { + this.behaviorRowFormGroup = this.fb.group({ + id: [null, [this.behaviorIdValidator()]], + name: [null, [Validators.required]], + type: [null, [Validators.required]] + }); + this.behaviorRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.behaviorRowFormGroup.get('type').valueChanges.subscribe((newType: ScadaSymbolBehaviorType) => { + this.onTypeChanged(newType); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.behaviorRowFormGroup.disable({emitEvent: false}); + } else { + this.behaviorRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ScadaSymbolBehavior): void { + this.modelValue = value; + this.behaviorRowFormGroup.patchValue( + { + id: value?.id, + name: value?.name, + type: value?.type + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + editBehavior($event: Event, matButton: MatButton, add = false, editCanceled = () => {}) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + isAdd: add, + disabled: this.disabled, + aliasController: this.aliasController, + callbacks: this.callbacks, + behavior: deepClone(this.modelValue) + }; + const scadaSymbolBehaviorPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ScadaSymbolBehaviorPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + scadaSymbolBehaviorPanelPopover.tbComponentRef.instance.popover = scadaSymbolBehaviorPanelPopover; + scadaSymbolBehaviorPanelPopover.tbComponentRef.instance.behaviorSettingsApplied.subscribe((behavior) => { + scadaSymbolBehaviorPanelPopover.hide(); + this.behaviorRowFormGroup.patchValue( + { + id: behavior.id, + name: behavior.name, + type: behavior.type + }, {emitEvent: false} + ); + this.modelValue = behavior; + this.propagateChange(this.modelValue); + }); + scadaSymbolBehaviorPanelPopover.tbDestroy.subscribe(() => { + if (!behaviorValid(this.modelValue)) { + editCanceled(); + } + }); + } + } + + focus() { + this.idInput.nativeElement.scrollIntoView(); + this.idInput.nativeElement.focus(); + } + + onAdd(onCanceled: () => void) { + this.idInput.nativeElement.scrollIntoView(); + this.editBehavior(null, this.editButton, true, onCanceled); + } + + public validate(_c: UntypedFormControl) { + const idControl = this.behaviorRowFormGroup.get('id'); + if (idControl.hasError('behaviorIdNotUnique')) { + idControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); + } + if (idControl.hasError('behaviorIdNotUnique')) { + this.behaviorRowFormGroup.get('id').markAsTouched(); + return { + behaviorIdNotUnique: true + }; + } + const behavior: ScadaSymbolBehavior = {...this.modelValue, ...this.behaviorRowFormGroup.value}; + if (!behaviorValid(behavior)) { + return { + behavior: true + }; + } + return null; + } + + private behaviorIdValidator(): ValidatorFn { + return control => { + if (!control.value) { + return { + required: true + }; + } + if (!this.behaviorsComponent.behaviorIdUnique(control.value, this.index)) { + return { + behaviorIdNotUnique: true + }; + } + return null; + }; + } + + private onTypeChanged(newType: ScadaSymbolBehaviorType) { + const prevModel = deepClone(this.modelValue); + this.modelValue = {...this.modelValue, ...{type: newType}}; + this.modelValue = updateBehaviorDefaultSettings(this.modelValue); + if (!behaviorValid(this.modelValue)) { + this.editBehavior(null, this.editButton, false, () => { + this.modelValue = prevModel; + this.behaviorRowFormGroup.patchValue( + { + type: prevModel.type + }, {emitEvent: true} + ); + }); + } + } + + private updateModel() { + const value: ScadaSymbolBehavior = this.behaviorRowFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html new file mode 100644 index 0000000000..7cefc6af63 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html @@ -0,0 +1,65 @@ + +
+
+
+
scada.behavior.id
+
scada.behavior.name
+
scada.behavior.type
+
+
+
+
+ + +
+ +
+
+
+ +
+
+ +
+
+ + + {{ 'scada.behavior.no-behaviors' | translate }} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.scss new file mode 100644 index 0000000000..11506a3c0e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.scss @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-behaviors { + flex: 1; + margin: 12px; + .tb-form-table-header-cell { + &.tb-id-header { + flex: 1 1 40%; + } + &.tb-name-header { + flex: 1 1 40%; + } + &.tb-type-header { + flex: 1 1 20%; + } + &.tb-actions-header { + width: 120px; + min-width: 120px; + &.disabled { + width: 40px; + min-width: 40px; + } + } + } + .tb-form-table { + overflow: hidden; + } + .tb-form-table-body { + overflow: auto; + tb-scada-symbol-metadata-behavior-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts new file mode 100644 index 0000000000..0ad1e59e05 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts @@ -0,0 +1,219 @@ +/// +/// 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, + forwardRef, + HostBinding, + Input, + OnInit, + QueryList, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { + defaultGetValueSettings, + ScadaSymbolBehavior, + ScadaSymbolBehaviorType +} from '@home/components/widget/lib/scada/scada-symbol.models'; +import { ValueType } from '@shared/models/constants'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { + behaviorValid, + ScadaSymbolBehaviorRowComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component'; +import { GetValueSettings, ValueToDataType } from '@shared/models/action-widget-settings.models'; +import { TranslateService } from '@ngx-translate/core'; +import { mergeDeep } from '@core/utils'; +import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; + +@Component({ + selector: 'tb-scada-symbol-metadata-behaviors', + templateUrl: './scada-symbol-behaviors.component.html', + styleUrls: ['./scada-symbol-behaviors.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolBehaviorsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolBehaviorsComponent implements ControlValueAccessor, OnInit, Validator { + + @HostBinding('style.display') styleDisplay = 'flex'; + @HostBinding('style.overflow') styleOverflow = 'hidden'; + + @ViewChildren(ScadaSymbolBehaviorRowComponent) + behaviorRows: QueryList; + + @Input() + aliasController: IAliasController; + + @Input() + callbacks: WidgetActionCallbacks; + + @Input() + disabled: boolean; + + behaviorsFormGroup: UntypedFormGroup; + + errorText = ''; + + get dragEnabled(): boolean { + return !this.disabled && this.behaviorsFormArray().controls.length > 1; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private translate: TranslateService) { + } + + ngOnInit() { + this.behaviorsFormGroup = this.fb.group({ + behaviors: this.fb.array([]) + }); + this.behaviorsFormGroup.valueChanges.subscribe( + () => { + let behaviors: ScadaSymbolBehavior[] = this.behaviorsFormGroup.get('behaviors').value; + if (behaviors) { + behaviors = behaviors.filter(b => behaviorValid(b)); + } + this.propagateChange(behaviors); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.behaviorsFormGroup.disable({emitEvent: false}); + } else { + this.behaviorsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ScadaSymbolBehavior[] | undefined): void { + const behaviors= value || []; + this.behaviorsFormGroup.setControl('behaviors', this.prepareBehaviorsFormArray(behaviors), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + this.errorText = ''; + const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; + const notUniqueControls = + behaviorsArray.controls.filter(control => control.hasError('behaviorIdNotUnique')); + for (const control of notUniqueControls) { + control.updateValueAndValidity({onlySelf: false, emitEvent: false}); + if (control.hasError('behaviorIdNotUnique')) { + this.errorText = this.translate.instant('scada.behavior.not-unique-behavior-ids-error'); + } + } + const valid = this.behaviorsFormGroup.valid; + return valid ? null : { + behaviors: { + valid: false, + }, + }; + } + + public behaviorIdUnique(id: string, index: number): boolean { + const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; + for (let i = 0; i < behaviorsArray.controls.length; i++) { + if (i !== index) { + const otherControl = behaviorsArray.controls[i]; + if (id === otherControl.value.id) { + return false; + } + } + } + return true; + } + + behaviorDrop(event: CdkDragDrop) { + const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; + const behavior = behaviorsArray.at(event.previousIndex); + behaviorsArray.removeAt(event.previousIndex); + behaviorsArray.insert(event.currentIndex, behavior); + } + + behaviorsFormArray(): UntypedFormArray { + return this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; + } + + trackByBehavior(index: number, behaviorControl: AbstractControl): any { + return behaviorControl; + } + + removeBehavior(index: number, emitEvent = true) { + (this.behaviorsFormGroup.get('behaviors') as UntypedFormArray).removeAt(index, {emitEvent}); + } + + addBehavior() { + const behavior: ScadaSymbolBehavior = { + id: '', + name: '', + type: ScadaSymbolBehaviorType.value, + valueType: ValueType.BOOLEAN, + defaultGetValueSettings: mergeDeep({} as GetValueSettings, defaultGetValueSettings(ValueType.BOOLEAN)) + }; + const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; + const behaviorControl = this.fb.control(behavior, []); + behaviorsArray.push(behaviorControl); + setTimeout(() => { + const behaviorRow = this.behaviorRows.get(this.behaviorRows.length-1); + behaviorRow.onAdd(() => { + this.removeBehavior(behaviorsArray.length-1); + }); + }); + } + + private prepareBehaviorsFormArray(behaviors: ScadaSymbolBehavior[] | undefined): UntypedFormArray { + const behaviorsControls: Array = []; + if (behaviors) { + behaviors.forEach((behavior) => { + behaviorsControls.push(this.fb.control(behavior, [])); + }); + } + return this.fb.array(behaviorsControls); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts new file mode 100644 index 0000000000..7507b4cdcb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts @@ -0,0 +1,75 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + ScadaSymbolMetadataTagComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component'; +import { + ScadaSymbolMetadataComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component'; +import { + ScadaSymbolMetadataTagsComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component'; +import { + ScadaSymbolBehaviorsComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component'; +import { + ScadaSymbolBehaviorRowComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component'; +import { + ScadaSymbolBehaviorPanelComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component'; +import { + ScadaSymbolPropertiesComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-properties.component'; +import { + ScadaSymbolPropertyRowComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component'; +import { + ScadaSymbolPropertyPanelComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component'; +import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module'; +import { + ScadaSymbolMetadataTagFunctionPanelComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component'; + +@NgModule({ + declarations: + [ + ScadaSymbolMetadataComponent, + ScadaSymbolMetadataTagComponent, + ScadaSymbolMetadataTagsComponent, + ScadaSymbolMetadataTagFunctionPanelComponent, + ScadaSymbolBehaviorsComponent, + ScadaSymbolBehaviorRowComponent, + ScadaSymbolBehaviorPanelComponent, + ScadaSymbolPropertiesComponent, + ScadaSymbolPropertyRowComponent, + ScadaSymbolPropertyPanelComponent + ], + imports: [ + CommonModule, + SharedModule, + WidgetSettingsCommonModule + ], + exports: [ + ScadaSymbolMetadataComponent + ] +}) +export class ScadaSymbolMetadataComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html new file mode 100644 index 0000000000..27c22df7a8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html @@ -0,0 +1,55 @@ + + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.scss new file mode 100644 index 0000000000..31b00469ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.scss @@ -0,0 +1,55 @@ +/** + * 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 '../../../../../../scss/constants'; + +.tb-scada-symbol-metadata-tag-function-panel { + width: 800px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-scada-symbol-metadata-tag-function-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + } + .tb-scada-symbol-metadata-tag-function-title-container { + display: flex; + flex-direction: row; + gap: 16px; + align-items: center; + } + .tb-scada-symbol-metadata-tag-function-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-scada-symbol-metadata-tag-function-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts new file mode 100644 index 0000000000..d46ba364d7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts @@ -0,0 +1,126 @@ +/// +/// 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 { + AfterViewInit, + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { WidgetService } from '@core/http/widget.service'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { TbHighlightRule } from '@shared/models/ace/ace.models'; +import { + scadaSymbolClickActionHighlightRules, + scadaSymbolClickActionPropertiesHighlightRules, + scadaSymbolElementStateRenderHighlightRules, + scadaSymbolElementStateRenderPropertiesHighlightRules +} from '@home/pages/scada-symbol/scada-symbol-editor.models'; +import { JsFuncComponent } from '@shared/components/js-func.component'; + +@Component({ + selector: 'tb-scada-symbol-metadata-tag-function-panel', + templateUrl: './scada-symbol-metadata-tag-function-panel.component.html', + styleUrls: ['./scada-symbol-metadata-tag-function-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, AfterViewInit { + + @ViewChild('tagFunctionComponent') + tagFunctionComponent: JsFuncComponent; + + @Input() + tagFunction: string; + + @Input() + tagFunctionType: 'renderFunction' | 'clickAction'; + + @Input() + tag: string; + + @Input() + completer: TbEditorCompleter; + + @Input() + disabled: boolean; + + @Input() + popover: TbPopoverComponent; + + @Output() + tagFunctionApplied = new EventEmitter(); + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + tagFunctionFormGroup: UntypedFormGroup; + + panelTitle: string; + + tagFunctionArgs: string[]; + + tagFunctionHelpId: string; + + objectHighlightRules: TbHighlightRule[]; + + propertyHighlightRules: TbHighlightRule[]; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService) { + } + + ngOnInit(): void { + this.tagFunctionFormGroup = this.fb.group( + { + tagFunction: [this.tagFunction, []] + } + ); + if (this.disabled) { + this.tagFunctionFormGroup.disable({emitEvent: false}); + } + if (this.tagFunctionType === 'renderFunction') { + this.panelTitle = 'scada.state-render-function'; + this.tagFunctionArgs = ['ctx', 'element']; + this.objectHighlightRules = scadaSymbolElementStateRenderHighlightRules; + this.propertyHighlightRules = scadaSymbolElementStateRenderPropertiesHighlightRules; + this.tagFunctionHelpId = 'scada/tag_state_render_fn'; + } else if (this.tagFunctionType === 'clickAction') { + this.panelTitle = 'scada.tag.on-click-action'; + this.tagFunctionArgs = ['ctx', 'element', 'event']; + this.objectHighlightRules = scadaSymbolClickActionHighlightRules; + this.propertyHighlightRules = scadaSymbolClickActionPropertiesHighlightRules; + this.tagFunctionHelpId = 'scada/tag_click_action_fn'; + } + } + + ngAfterViewInit() { + this.tagFunctionComponent.focus(); + } + + cancel() { + this.popover?.hide(); + } + + applyTagFunction() { + const tagFunction: string = this.tagFunctionFormGroup.get('tagFunction').value; + this.tagFunctionApplied.emit(tagFunction); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html new file mode 100644 index 0000000000..2b04dfaf13 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html @@ -0,0 +1,48 @@ + + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss new file mode 100644 index 0000000000..08296a986e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-metadata-tag-row { + .tb-tag-field { + flex: 1 1 70%; + } + .tb-state-render-function-field { + flex: 1 1 15%; + } + .tb-on-click-action-field { + flex: 1 1 15%; + } + .tb-state-render-function-field, .tb-on-click-action-field { + display: flex; + justify-content: flex-end; + .mdc-button { + .mat-mdc-button-touch-target { + height: 36px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts new file mode 100644 index 0000000000..3894838bb6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts @@ -0,0 +1,200 @@ +/// +/// 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, + forwardRef, + Input, + OnInit, + Renderer2, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { ScadaSymbolTag } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { + ScadaSymbolMetadataTagFunctionPanelComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component'; + +@Component({ + selector: 'tb-scada-symbol-metadata-tag', + templateUrl: './scada-symbol-metadata-tag.component.html', + styleUrls: ['./scada-symbol-metadata-tag.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolMetadataTagComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolMetadataTagComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, OnInit, Validator { + + @ViewChild('editStateRenderFunctionButton') + editStateRenderFunctionButton: MatButton; + + @ViewChild('editClickActionButton') + editClickActionButton: MatButton; + + @Input() + disabled: boolean; + + @Input() + elementStateRenderFunctionCompleter: TbEditorCompleter; + + @Input() + clickActionFunctionCompleter: TbEditorCompleter; + + tagFormGroup: UntypedFormGroup; + + modelValue: ScadaSymbolTag; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) { + } + + ngOnInit() { + this.tagFormGroup = this.fb.group({ + tag: [null, []], + stateRenderFunction: [null, []], + clickAction: [null, []] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.tagFormGroup.disable({emitEvent: false}); + } else { + this.tagFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ScadaSymbolTag): void { + this.modelValue = value; + const clickAction = value?.actions?.click?.actionFunction; + this.tagFormGroup.patchValue( + { + tag: value?.tag, + stateRenderFunction: value?.stateRenderFunction, + clickAction + }, {emitEvent: false} + ); + } + + public validate(_c: UntypedFormControl) { + const valid = this.tagFormGroup.valid; + return valid ? null : { + tag: { + valid: false, + }, + }; + } + + editTagStateRenderFunction(): void { + this.openTagFunction('renderFunction', this.editStateRenderFunctionButton); + } + + editClickAction(): void { + this.openTagFunction('clickAction', this.editClickActionButton); + } + + private openTagFunction(tagFunctionType: 'renderFunction' | 'clickAction', + button: MatButton) { + const trigger = button._elementRef.nativeElement; + trigger.scrollIntoView(); + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + let tagFunctionControl: AbstractControl; + let completer: TbEditorCompleter; + if (tagFunctionType === 'renderFunction') { + tagFunctionControl = this.tagFormGroup.get('stateRenderFunction'); + completer = this.elementStateRenderFunctionCompleter; + } else if (tagFunctionType === 'clickAction') { + tagFunctionControl = this.tagFormGroup.get('clickAction'); + completer = this.clickActionFunctionCompleter; + } + const ctx: any = { + tagFunction: tagFunctionControl.value, + tagFunctionType, + tag: this.tagFormGroup.get('tag').value, + completer, + disabled: this.disabled + }; + const scadaSymbolTagFunctionPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ScadaSymbolMetadataTagFunctionPanelComponent, + ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + scadaSymbolTagFunctionPanelPopover.tbComponentRef.instance.popover = scadaSymbolTagFunctionPanelPopover; + scadaSymbolTagFunctionPanelPopover.tbComponentRef.instance.tagFunctionApplied.subscribe((tagFunction) => { + scadaSymbolTagFunctionPanelPopover.hide(); + tagFunctionControl.patchValue(tagFunction, {emitEvent: false}); + this.updateModel(); + }); + } + } + + private updateModel() { + const value = this.tagFormGroup.value; + this.modelValue = { + tag: value.tag, + stateRenderFunction: value.stateRenderFunction + }; + if (value.clickAction) { + this.modelValue.actions = { + click: { + actionFunction: value.clickAction + } + }; + } else { + this.modelValue.actions = null; + } + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html new file mode 100644 index 0000000000..ec0f69a7f0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html @@ -0,0 +1,40 @@ + + + + {{ 'scada.tag.no-tags' | translate }} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss new file mode 100644 index 0000000000..1c4238a11d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-metadata-tags { + flex: 1; + margin: 12px; + .tb-form-table-header-cell { + &.tb-tag-header { + flex: 1 1 70%; + } + &.tb-state-render-function-header { + flex: 1 1 15%; + } + &.tb-on-click-action-header { + flex: 1 1 15%; + } + &.tb-state-render-function-header, &.tb-on-click-action-header { + display: flex; + justify-content: flex-end; + } + } + .tb-form-table { + overflow: hidden; + } + .tb-form-table-body { + overflow: auto; + tb-scada-symbol-metadata-tag { + overflow: hidden; + } + .mat-divider { + margin-top: 8px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts new file mode 100644 index 0000000000..42dfe8b7e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts @@ -0,0 +1,231 @@ +/// +/// 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, + forwardRef, + HostBinding, + Input, + OnChanges, + OnInit, + QueryList, + SimpleChanges, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { ScadaSymbolTag } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { + ScadaSymbolMetadataTagComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; + +const tagIsEmpty = (tag: ScadaSymbolTag): boolean => + !tag.stateRenderFunction && !tag.actions?.click?.actionFunction; + +@Component({ + selector: 'tb-scada-symbol-metadata-tags', + templateUrl: './scada-symbol-metadata-tags.component.html', + styleUrls: ['./scada-symbol-metadata-tags.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, OnInit, Validator, OnChanges { + + @HostBinding('style.display') styleDisplay = 'flex'; + @HostBinding('style.overflow') styleOverflow = 'hidden'; + + @ViewChildren(ScadaSymbolMetadataTagComponent) + metadataTags: QueryList; + + @Input() + disabled: boolean; + + @Input() + tags: string[]; + + @Input() + elementStateRenderFunctionCompleter: TbEditorCompleter; + + @Input() + clickActionFunctionCompleter: TbEditorCompleter; + + tagsFormGroup: UntypedFormGroup; + + private modelValue: ScadaSymbolTag[]; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.tagsFormGroup = this.fb.group({ + tags: this.fb.array([]) + }); + const tagsResult = this.setupTags(); + this.tagsFormGroup.setControl('tags', this.prepareTagsFormArray(tagsResult.tags), {emitEvent: false}); + + this.tagsFormGroup.valueChanges.subscribe( + () => { + let value: ScadaSymbolTag[] = this.tagsFormGroup.get('tags').value; + if (value) { + value = value.filter(t => !tagIsEmpty(t)); + } + this.modelValue = value; + this.propagateChange(this.modelValue); + } + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['tags'].includes(propName)) { + const tagsResult = this.setupTags(this.modelValue); + const tagsControls = this.prepareTagsFormArray(tagsResult.tags); + if (tagsResult.emitEvent) { + setTimeout(() => { + this.tagsFormGroup.setControl('tags', tagsControls, {emitEvent: true}); + this.setDisabledState(this.disabled); + }); + } else { + this.tagsFormGroup.setControl('tags', tagsControls, {emitEvent: false}); + this.setDisabledState(this.disabled); + } + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.tagsFormGroup.disable({emitEvent: false}); + } else { + this.tagsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ScadaSymbolTag[] | undefined): void { + this.modelValue = value || []; + const tagsResult= this.setupTags(this.modelValue); + this.tagsFormGroup.setControl('tags', this.prepareTagsFormArray(tagsResult.tags), {emitEvent: false}); + this.setDisabledState(this.disabled); + } + + public validate(_c: UntypedFormControl) { + const valid = this.tagsFormGroup.valid; + return valid ? null : { + tags: { + valid: false, + }, + }; + } + + tagsFormArray(): UntypedFormArray { + return this.tagsFormGroup.get('tags') as UntypedFormArray; + } + + trackByTag(_index: number, tagControl: AbstractControl): any { + return tagControl; + } + + editTagStateRenderFunction(tag: string): void { + setTimeout(() => { + const tags: ScadaSymbolTag[] = this.tagsFormGroup.get('tags').value; + const index = tags.findIndex(t => t.tag === tag); + const tagComponent = this.metadataTags.get(index); + tagComponent?.editTagStateRenderFunction(); + }); + } + + editTagClickAction(tag: string): void { + setTimeout(() => { + const tags: ScadaSymbolTag[] = this.tagsFormGroup.get('tags').value; + const index = tags.findIndex(t => t.tag === tag); + const tagComponent = this.metadataTags.get(index); + tagComponent?.editClickAction(); + }); + } + + private setupTags(existing?: ScadaSymbolTag[]): {tags: ScadaSymbolTag[]; emitEvent: boolean} { + existing = (existing || []).filter(t => !tagIsEmpty(t)); + const result = (this.tags || []).sort().map(tag => ({ + tag, + stateRenderFunction: null, + actions: null + })); + for (const tag of existing) { + const found = result.find(t => t.tag === tag.tag); + if (found) { + found.stateRenderFunction = tag.stateRenderFunction; + if (tag.actions?.click?.actionFunction) { + found.actions = { + click: { + actionFunction: tag.actions.click.actionFunction + } + }; + } + } + } + const tagRemoved = !!existing.find(existingTag => + !result.find(t => t.tag === existingTag.tag)); + return { + tags: result, + emitEvent: tagRemoved + }; + } + + private prepareTagsFormArray(tags: ScadaSymbolTag[] | undefined): UntypedFormArray { + const tagsControls: Array = []; + if (tags) { + tags.forEach((tag) => { + tagsControls.push(this.fb.control(tag, [])); + }); + } + return this.fb.array(tagsControls); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html new file mode 100644 index 0000000000..9e5d22fa94 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html @@ -0,0 +1,115 @@ + + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss new file mode 100644 index 0000000000..370984230b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.scss @@ -0,0 +1,116 @@ +/** + * 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 '../../../../../../scss/constants'; + +.tb-scada-symbol-metadata { + display: flex; + flex-direction: column; + gap: 8px; + .tb-scada-symbol-metadata-header { + padding: 24px 24px 0; + display: flex; + gap: 12px; + flex-direction: column-reverse; + align-items: flex-start; + @media #{$mat-gt-sm} { + gap: 0; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + .tb-scada-symbol-metadata-header-components { + width: 100%; + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + } + .tb-scada-symbol-metadata-content { + display: flex; + & > .mat-content { + padding-top: 8px; + @media #{$mat-xs} { + padding-left: 8px; + padding-right: 8px; + } + } + flex: 1; + overflow: auto; + & > div { + padding: 16px; + } + .mat-content { + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + gap: 8px; + } + &.overflow-hidden { + overflow: hidden; + } + } + .tb-form-panel { + .mat-expansion-panel.tb-settings { + & > .mat-expansion-panel-header { + font-weight: 400; + } + } + } + } +} + +.tb-js-func { + .ace_tb { + &.ace_scada-symbol { + &-ctx { + color: #C52F00; + &-properties, &-property { + color: #185F2A; + } + &-values, &-value { + color: #2CAA00; + } + &-tags, &-tag { + color: #DF8600; + } + &-api, &-api-method { + color: #7214D0; + } + &-svg, &-svg-method { + color: #c24c1a; + } + &-properties, &-values, &-tags, &-api { + // font-style: italic; + // font-weight: bold; + } + &-property, &-value, &-tag, &-api-method { + font-weight: 700; + } + } + &-svg, &-element, &-event { + color: #C52F00; + // font-weight: bold; + &-properties { + color: #7214D0; + font-weight: 700; + } + } + } + } +} 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 new file mode 100644 index 0000000000..f5ecaab3d2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -0,0 +1,247 @@ +/// +/// 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, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { emptyMetadata, ScadaSymbolMetadata } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; +import { TranslateService } from '@ngx-translate/core'; +import { + ScadaSymbolMetadataTagsComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { + clickActionFunctionCompletions, + elementStateRenderFunctionCompletions, + generalStateRenderFunctionCompletions, + scadaSymbolContextCompletion, + scadaSymbolGeneralStateRenderHighlightRules, + scadaSymbolGeneralStateRenderPropertiesHighlightRules +} from '@home/pages/scada-symbol/scada-symbol-editor.models'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; + +@Component({ + selector: 'tb-scada-symbol-metadata', + templateUrl: './scada-symbol-metadata.component.html', + styleUrls: ['./scada-symbol-metadata.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolMetadataComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolMetadataComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolMetadataComponent extends PageComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { + + @ViewChild('symbolMetadataTags') + symbolMetadataTags: ScadaSymbolMetadataTagsComponent; + + @Input() + aliasController: IAliasController; + + @Input() + callbacks: WidgetActionCallbacks; + + @Input() + disabled: boolean; + + @Input() + tags: string[]; + + private modelValue: ScadaSymbolMetadata; + + private propagateChange = null; + + public metadataFormGroup: UntypedFormGroup; + + headerOptions: ToggleHeaderOption[] = [ + { + name: this.translate.instant('scada.general'), + value: 'general' + }, + { + name: this.translate.instant('scada.tags'), + value: 'tags' + }, + { + name: this.translate.instant('scada.behavior.behavior'), + value: 'behavior' + }, + { + name: this.translate.instant('scada.properties'), + value: 'properties' + } + ]; + + selectedOption = 'general'; + + generalStateRenderFunctionCompleter: TbEditorCompleter; + elementStateRenderFunctionCompleter: TbEditorCompleter; + clickActionFunctionCompleter: TbEditorCompleter; + + scadaSymbolGeneralStateRenderHighlightRules = scadaSymbolGeneralStateRenderHighlightRules; + + scadaSymbolGeneralStateRenderPropertiesHighlightRules = scadaSymbolGeneralStateRenderPropertiesHighlightRules; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + private translate: TranslateService, + private customTranslate: CustomTranslatePipe) { + super(store); + } + + ngOnInit(): void { + this.metadataFormGroup = this.fb.group({ + title: [null, [Validators.required]], + description: [null], + searchTags: [null], + widgetSizeX: [null, [Validators.min(1), Validators.max(24), Validators.required]], + widgetSizeY: [null, [Validators.min(1), Validators.max(24), Validators.required]], + stateRenderFunction: [null], + tags: [null], + behavior: [null], + properties: [null] + }); + this.updateFunctionCompleters(emptyMetadata()); + + this.metadataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['tags'].includes(propName)) { + this.updateFunctionCompleters(this.modelValue); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.metadataFormGroup.disable({emitEvent: false}); + } else { + this.metadataFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ScadaSymbolMetadata): void { + this.modelValue = value; + this.metadataFormGroup.patchValue( + { + title: value?.title, + description: value?.description, + searchTags: value?.searchTags, + widgetSizeX: value?.widgetSizeX || 3, + widgetSizeY: value?.widgetSizeY || 3, + stateRenderFunction: value?.stateRenderFunction, + tags: value?.tags, + behavior: value?.behavior, + properties: value?.properties + }, {emitEvent: false} + ); + this.updateFunctionCompleters(value); + } + + editTagStateRenderFunction(tag: string): void { + this.selectedOption = 'tags'; + this.symbolMetadataTags.editTagStateRenderFunction(tag); + } + + editTagClickAction(tag: string): void { + this.selectedOption = 'tags'; + this.symbolMetadataTags.editTagClickAction(tag); + } + + public validate(c: UntypedFormControl) { + const valid = this.metadataFormGroup.valid; + return valid ? null : { + metadata: { + valid: false, + }, + }; + } + + private updateModel() { + const metadata: ScadaSymbolMetadata = this.metadataFormGroup.getRawValue(); + this.modelValue = metadata; + this.propagateChange(this.modelValue); + this.updateFunctionCompleters(metadata); + } + + private updateFunctionCompleters(metadata: ScadaSymbolMetadata) { + const contextCompleter = scadaSymbolContextCompletion(metadata, this.tags, this.customTranslate); + const generalStateRender = generalStateRenderFunctionCompletions(contextCompleter); + if (!this.generalStateRenderFunctionCompleter) { + this.generalStateRenderFunctionCompleter = new TbEditorCompleter(generalStateRender); + } else { + this.generalStateRenderFunctionCompleter.updateCompletions(generalStateRender); + } + const elementStateRender = elementStateRenderFunctionCompletions(contextCompleter); + if (!this.elementStateRenderFunctionCompleter) { + this.elementStateRenderFunctionCompleter = new TbEditorCompleter(elementStateRender); + } else { + this.elementStateRenderFunctionCompleter.updateCompletions(elementStateRender); + } + const clickAction = clickActionFunctionCompletions(contextCompleter); + if (!this.clickActionFunctionCompleter) { + this.clickActionFunctionCompleter = new TbEditorCompleter(clickAction); + } else { + this.clickActionFunctionCompleter.updateCompletions(clickAction); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html new file mode 100644 index 0000000000..1b553a5130 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html @@ -0,0 +1,64 @@ + +
+
+
+
scada.property.id
+
scada.property.name
+
scada.property.type
+
+
+
+
+ + +
+ +
+
+
+ +
+
+ +
+
+ + + {{ 'scada.property.no-properties' | translate }} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss new file mode 100644 index 0000000000..094ea0668b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-properties { + flex: 1; + margin: 12px; + .tb-form-table-header-cell { + &.tb-id-header { + flex: 1 1 40%; + } + &.tb-name-header { + flex: 1 1 40%; + } + &.tb-type-header { + flex: 1 1 20%; + } + &.tb-actions-header { + width: 120px; + min-width: 120px; + &.disabled { + width: 40px; + min-width: 40px; + } + } + } + .tb-form-table { + overflow: hidden; + } + .tb-form-table-body { + overflow: auto; + tb-scada-symbol-metadata-property-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts new file mode 100644 index 0000000000..840302161b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts @@ -0,0 +1,214 @@ +/// +/// 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, + forwardRef, + HostBinding, + Input, + OnInit, + QueryList, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { ScadaSymbolProperty, ScadaSymbolPropertyType } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { + propertyValid, + ScadaSymbolPropertyRowComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-scada-symbol-metadata-properties', + templateUrl: './scada-symbol-properties.component.html', + styleUrls: ['./scada-symbol-properties.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolPropertiesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolPropertiesComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnInit, Validator { + + @HostBinding('style.display') styleDisplay = 'flex'; + @HostBinding('style.overflow') styleOverflow = 'hidden'; + + @ViewChildren(ScadaSymbolPropertyRowComponent) + propertyRows: QueryList; + + @Input() + disabled: boolean; + + booleanPropertyIds: string[] = []; + + propertiesFormGroup: UntypedFormGroup; + + errorText = ''; + + get dragEnabled(): boolean { + return !this.disabled && this.propertiesFormArray().controls.length > 1; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private translate: TranslateService) { + } + + ngOnInit() { + this.propertiesFormGroup = this.fb.group({ + properties: this.fb.array([]) + }); + this.propertiesFormGroup.valueChanges.subscribe( + () => { + let properties: ScadaSymbolProperty[] = this.propertiesFormGroup.get('properties').value; + if (properties) { + properties = properties.filter(p => propertyValid(p)); + } + this.booleanPropertyIds = properties.filter(p => p.type === ScadaSymbolPropertyType.switch).map(p => p.id); + properties.forEach((p, i) => { + if (p.disableOnProperty && !this.booleanPropertyIds.includes(p.disableOnProperty)) { + p.disableOnProperty = null; + const controls = this.propertiesFormArray().controls; + controls[i].patchValue(p, {emitEvent: false}); + } + }); + this.propagateChange(properties); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.propertiesFormGroup.disable({emitEvent: false}); + } else { + this.propertiesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ScadaSymbolProperty[] | undefined): void { + const properties= value || []; + this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(properties), {emitEvent: false}); + this.booleanPropertyIds = properties.filter(p => p.type === ScadaSymbolPropertyType.switch).map(p => p.id); + } + + public validate(c: UntypedFormControl) { + this.errorText = ''; + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const notUniqueControls = + propertiesArray.controls.filter(control => control.hasError('propertyIdNotUnique')); + for (const control of notUniqueControls) { + control.updateValueAndValidity({onlySelf: false, emitEvent: false}); + if (control.hasError('propertyIdNotUnique')) { + this.errorText = this.translate.instant('scada.property.not-unique-property-ids-error'); + } + } + const valid = this.propertiesFormGroup.valid; + return valid ? null : { + properties: { + valid: false, + }, + }; + } + + public propertyIdUnique(id: string, index: number): boolean { + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + for (let i = 0; i < propertiesArray.controls.length; i++) { + if (i !== index) { + const otherControl = propertiesArray.controls[i]; + if (id === otherControl.value.id) { + return false; + } + } + } + return true; + } + + propertyDrop(event: CdkDragDrop) { + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const property = propertiesArray.at(event.previousIndex); + propertiesArray.removeAt(event.previousIndex); + propertiesArray.insert(event.currentIndex, property); + } + + propertiesFormArray(): UntypedFormArray { + return this.propertiesFormGroup.get('properties') as UntypedFormArray; + } + + trackByProperty(index: number, propertyControl: AbstractControl): any { + return propertyControl; + } + + removeProperty(index: number, emitEvent = true) { + (this.propertiesFormGroup.get('properties') as UntypedFormArray).removeAt(index, {emitEvent}); + } + + addProperty() { + const property: ScadaSymbolProperty = { + id: '', + name: '', + type: ScadaSymbolPropertyType.text, + default: '' + }; + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const propertyControl = this.fb.control(property, []); + propertiesArray.push(propertyControl); + setTimeout(() => { + const propertyRow = this.propertyRows.get(this.propertyRows.length-1); + propertyRow.onAdd(() => { + this.removeProperty(propertiesArray.length-1); + }); + }); + } + + private preparePropertiesFormArray(properties: ScadaSymbolProperty[] | undefined): UntypedFormArray { + const propertiesControls: Array = []; + if (properties) { + properties.forEach((property) => { + propertiesControls.push(this.fb.control(property, [])); + }); + } + return this.fb.array(propertiesControls); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html new file mode 100644 index 0000000000..9fd461c229 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html @@ -0,0 +1,185 @@ + +
+
{{ panelTitle | translate }}
+
+
+
scada.property.id
+ + + +
+
+
scada.property.name
+ + + +
+
+
scada.property.type
+ + + + {{ scadaSymbolPropertyTypeTranslations.get(type) | translate }} + + + +
+
+
scada.property.default-value
+ + + + + + + + + + + + + + +
+
+
scada.property.number-settings
+
+
scada.property.min
+ + + + +
scada.property.max
+ + + + +
scada.property.step
+ + + +
+
+
+ + {{ 'scada.property.value-required' | translate }} + +
+
+ + + + {{ 'scada.property.advanced-ui-settings' | translate }} + + + +
+
scada.property.sub-label
+ + + +
+
+ + {{ 'scada.property.vertical-divider-after' | translate }} + +
+
+
scada.property.input-field-suffix
+ + + +
+
+
scada.property.disable-on-property
+ + + + {{ prop }} + + + +
+
+
scada.property.property-row-classes
+ + + + {{ clazz }} + + + +
+
+
scada.property.property-field-classes
+ + + + {{ clazz }} + + + +
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss new file mode 100644 index 0000000000..c4b5b153d9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss @@ -0,0 +1,49 @@ +/** + * 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 '../../../../../../scss/constants'; + +.tb-scada-symbol-property-settings-panel { + width: 540px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-scada-symbol-property-settings-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + } + .tb-scada-symbol-property-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-scada-symbol-property-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts new file mode 100644 index 0000000000..4c59787c0f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts @@ -0,0 +1,136 @@ +/// +/// 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, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { + ScadaSymbolProperty, + scadaSymbolPropertyFieldClasses, + scadaSymbolPropertyRowClasses, + ScadaSymbolPropertyType, + scadaSymbolPropertyTypes, + scadaSymbolPropertyTypeTranslations +} from '@home/components/widget/lib/scada/scada-symbol.models'; +import { defaultPropertyValue } from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component'; +import { ValueType } from '@shared/models/constants'; + +@Component({ + selector: 'tb-scada-symbol-property-panel', + templateUrl: './scada-symbol-property-panel.component.html', + styleUrls: ['./scada-symbol-property-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolPropertyPanelComponent implements OnInit { + + ValueType = ValueType; + + ScadaSymbolPropertyType = ScadaSymbolPropertyType; + + scadaSymbolPropertyTypes = scadaSymbolPropertyTypes; + scadaSymbolPropertyTypeTranslations = scadaSymbolPropertyTypeTranslations; + + scadaSymbolPropertyRowClasses = scadaSymbolPropertyRowClasses; + + scadaSymbolPropertyFieldClasses = scadaSymbolPropertyFieldClasses; + + @Input() + isAdd = false; + + @Input() + property: ScadaSymbolProperty; + + @Input() + booleanPropertyIds: string[]; + + @Input() + disabled: boolean; + + @Input() + popover: TbPopoverComponent; + + @Output() + propertySettingsApplied = new EventEmitter(); + + panelTitle: string; + + propertyFormGroup: UntypedFormGroup; + + private propertyType: ScadaSymbolPropertyType; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit(): void { + this.panelTitle = this.isAdd ? 'scada.property.add-property' : 'scada.property.property-settings'; + this.propertyType = this.property.type; + this.propertyFormGroup = this.fb.group( + { + id: [this.property.id, [Validators.required]], + name: [this.property.name, [Validators.required]], + type: [this.property.type, [Validators.required]], + default: [this.property.default, []], + required: [this.property.required, []], + subLabel: [this.property.subLabel, []], + divider: [this.property.divider, []], + fieldSuffix: [this.property.fieldSuffix, []], + disableOnProperty: [this.property.disableOnProperty, []], + rowClass: [(this.property.rowClass || '').split(' '), []], + fieldClass: [(this.property.fieldClass || '').split(' '), []], + min: [this.property.min, []], + max: [this.property.max, []], + step: [this.property.step, [Validators.min(0)]] + } + ); + if (this.disabled) { + this.propertyFormGroup.disable({emitEvent: false}); + } else { + this.propertyFormGroup.get('type').valueChanges.subscribe(() => { + this.updateValidators(); + }); + this.updateValidators(); + } + } + + cancel() { + this.popover?.hide(); + } + + applyPropertySettings() { + const property = this.propertyFormGroup.getRawValue(); + property.rowClass = (property.rowClass || []).join(' '); + property.fieldClass = (property.fieldClass || []).join(' '); + this.propertySettingsApplied.emit(property); + } + + private updateValidators() { + const type: ScadaSymbolPropertyType = this.propertyFormGroup.get('type').value; + if (type === ScadaSymbolPropertyType.number) { + this.propertyFormGroup.get('min').enable({emitEvent: false}); + this.propertyFormGroup.get('max').enable({emitEvent: false}); + this.propertyFormGroup.get('step').enable({emitEvent: false}); + } else { + this.propertyFormGroup.get('min').disable({emitEvent: false}); + this.propertyFormGroup.get('max').disable({emitEvent: false}); + this.propertyFormGroup.get('step').disable({emitEvent: false}); + } + if (this.propertyType !== type) { + const defaultValue = defaultPropertyValue(type); + this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false}); + this.propertyType = type; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html new file mode 100644 index 0000000000..bb63e69d68 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html @@ -0,0 +1,50 @@ + + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss new file mode 100644 index 0000000000..5c0b4f0166 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-metadata-property-row { + .tb-id-field { + flex: 1 1 40%; + } + .tb-name-field { + flex: 1 1 40%; + } + .tb-type-field { + flex: 1 1 20%; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts new file mode 100644 index 0000000000..3c5ea6cb07 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts @@ -0,0 +1,281 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + Renderer2, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { + ScadaSymbolProperty, + ScadaSymbolPropertyType, + scadaSymbolPropertyTypes, + scadaSymbolPropertyTypeTranslations +} from '@home/components/widget/lib/scada/scada-symbol.models'; +import { deepClone } from '@core/utils'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { constantColor, Font } from '@shared/models/widget-settings.models'; +import { + ScadaSymbolPropertyPanelComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component'; +import { + ScadaSymbolPropertiesComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-properties.component'; + +export const propertyValid = (property: ScadaSymbolProperty): boolean => !(!property.id || !property.name || !property.type); + +export const defaultPropertyValue = (type: ScadaSymbolPropertyType): any => { + switch (type) { + case ScadaSymbolPropertyType.text: + return ''; + case ScadaSymbolPropertyType.number: + return 0; + case ScadaSymbolPropertyType.switch: + return false; + case ScadaSymbolPropertyType.color: + return '#000'; + case ScadaSymbolPropertyType.color_settings: + return constantColor('#000'); + case ScadaSymbolPropertyType.font: + return { + size: 12, + sizeUnit: 'px', + family: 'Roboto', + weight: 'normal', + style: 'normal', + lineHeight: '1' + } as Font; + case ScadaSymbolPropertyType.units: + return ''; + } +}; + +@Component({ + selector: 'tb-scada-symbol-metadata-property-row', + templateUrl: './scada-symbol-property-row.component.html', + styleUrls: ['./scada-symbol-property-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, OnInit, Validator { + + @ViewChild('idInput') + idInput: ElementRef; + + @ViewChild('editButton') + editButton: MatButton; + + scadaSymbolPropertyTypes = scadaSymbolPropertyTypes; + scadaSymbolPropertyTypeTranslations = scadaSymbolPropertyTypeTranslations; + + @Input() + disabled: boolean; + + @Input() + index: number; + + @Input() + booleanPropertyIds: string[]; + + @Output() + propertyRemoved = new EventEmitter(); + + propertyRowFormGroup: UntypedFormGroup; + + modelValue: ScadaSymbolProperty; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private propertiesComponent: ScadaSymbolPropertiesComponent) { + } + + ngOnInit() { + this.propertyRowFormGroup = this.fb.group({ + id: [null, [this.propertyIdValidator()]], + name: [null, [Validators.required]], + type: [null, [Validators.required]] + }); + this.propertyRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.propertyRowFormGroup.get('type').valueChanges.subscribe((newType: ScadaSymbolPropertyType) => { + this.onTypeChanged(newType); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.propertyRowFormGroup.disable({emitEvent: false}); + } else { + this.propertyRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ScadaSymbolProperty): void { + this.modelValue = value; + this.propertyRowFormGroup.patchValue( + { + id: value?.id, + name: value?.name, + type: value?.type + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + editProperty($event: Event, matButton: MatButton, add = false, editCanceled = () => {}) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + isAdd: add, + disabled: this.disabled, + booleanPropertyIds: this.booleanPropertyIds, + property: deepClone(this.modelValue) + }; + const scadaSymbolPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ScadaSymbolPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + scadaSymbolPropertyPanelPopover.tbComponentRef.instance.popover = scadaSymbolPropertyPanelPopover; + scadaSymbolPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => { + scadaSymbolPropertyPanelPopover.hide(); + this.propertyRowFormGroup.patchValue( + { + id: property.id, + name: property.name, + type: property.type + }, {emitEvent: false} + ); + this.modelValue = property; + this.propagateChange(this.modelValue); + }); + scadaSymbolPropertyPanelPopover.tbDestroy.subscribe(() => { + if (!propertyValid(this.modelValue)) { + editCanceled(); + } + }); + } + } + + focus() { + this.idInput.nativeElement.scrollIntoView(); + this.idInput.nativeElement.focus(); + } + + onAdd(onCanceled: () => void) { + this.idInput.nativeElement.scrollIntoView(); + this.editProperty(null, this.editButton, true, onCanceled); + } + + public validate(c: UntypedFormControl) { + const idControl = this.propertyRowFormGroup.get('id'); + if (idControl.hasError('propertyIdNotUnique')) { + idControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); + } + if (idControl.hasError('propertyIdNotUnique')) { + this.propertyRowFormGroup.get('id').markAsTouched(); + return { + propertyIdNotUnique: true + }; + } + const property: ScadaSymbolProperty = {...this.modelValue, ...this.propertyRowFormGroup.value}; + if (!propertyValid(property)) { + return { + property: true + }; + } + return null; + } + + private propertyIdValidator(): ValidatorFn { + return control => { + if (!control.value) { + return { + required: true + }; + } + if (!this.propertiesComponent.propertyIdUnique(control.value, this.index)) { + return { + propertyIdNotUnique: true + }; + } + return null; + }; + } + + private onTypeChanged(newType: ScadaSymbolPropertyType) { + this.modelValue = {...this.modelValue, ...{type: newType}}; + this.modelValue.default = defaultPropertyValue(newType); + } + + private updateModel() { + const value: ScadaSymbolProperty = this.propertyRowFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + +} 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 new file mode 100644 index 0000000000..4eaaf6d718 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html @@ -0,0 +1,67 @@ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+ +
+
+ +
+
+
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 new file mode 100644 index 0000000000..c400a32fef --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss @@ -0,0 +1,224 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-editor-shape { + width: 100%; + height: 100%; + display: flex; + 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-primary-fill { + background: #fff; + border-radius: 50%; + &:before { + opacity: 0.1; + } + } + + .tb-scada-symbol-editor-view-buttons { + display: flex; + flex-direction: column; + gap: 8px; + z-index: 101; + } + + .tb-scada-symbol-editor-zoom-buttons { + display: flex; + flex-direction: column; + z-index: 101; + background: #fff; + border-radius: 24px; + &.tb-primary-fill { + border-radius: 24px; + &:before { + opacity: 0.1; + } + } + } + + .tb-scada-symbol-editor-upload-buttons { + display: flex; + flex-direction: row; + gap: 8px; + z-index: 101; + } +} + +.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 new file mode 100644 index 0000000000..9228c3e561 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts @@ -0,0 +1,165 @@ +/// +/// 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 { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, + ViewChild, + 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; +} + +@Component({ + selector: 'tb-scada-symbol-editor', + templateUrl: './scada-symbol-editor.component.html', + styleUrls: ['./scada-symbol-editor.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewInit, OnChanges { + + @ViewChild('scadaSymbolShape', {static: false}) + scadaSymbolShape: ElementRef; + + @ViewChild('tooltipsContainer', {static: false}) + tooltipsContainer: ElementRef; + + @ViewChild('tooltipsContainerComponent', {static: true}) + tooltipsContainerComponent: TbAnchorComponent; + + @Input() + data: ScadaSymbolEditorData; + + @Input() + editObjectCallbacks: ScadaSymbolEditObjectCallbacks; + + @Input() + readonly: boolean; + + @Output() + updateScadaSymbol = new EventEmitter(); + + @Output() + downloadScadaSymbol = new EventEmitter(); + + scadaSymbolEditObject: ScadaSymbolEditObject; + + zoomInDisabled = false; + zoomOutDisabled = false; + + @Input() + showHiddenElements = false; + + @Output() + showHiddenElementsChange = new EventEmitter(); + + displayShowHidden = false; + + constructor(private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + } + + ngAfterViewInit() { + this.editObjectCallbacks.onZoom = () => { + this.updateZoomButtonsState(); + }; + this.editObjectCallbacks.hasHiddenElements = (hasHidden) => { + this.displayShowHidden = hasHidden; + if (hasHidden) { + this.scadaSymbolEditObject.showHiddenElements(this.showHiddenElements); + } + this.cd.markForCheck(); + }; + this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.scadaSymbolShape.nativeElement, + this.tooltipsContainer.nativeElement, + this.tooltipsContainerComponent.viewContainerRef, this.editObjectCallbacks, this.readonly); + if (this.data) { + this.updateContent(this.data.scadaSymbolContent); + } + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'data') { + if (this.scadaSymbolEditObject) { + setTimeout(() => { + this.updateContent(this.data.scadaSymbolContent); + }); + } + } else if (propName === 'readonly') { + this.scadaSymbolEditObject.setReadOnly(this.readonly); + } + } + } + } + + ngOnDestroy() { + this.scadaSymbolEditObject.destroy(); + } + + getContent(): string { + return this.scadaSymbolEditObject?.getContent(); + } + + zoomIn() { + this.scadaSymbolEditObject.zoomIn(); + } + + zoomOut() { + this.scadaSymbolEditObject.zoomOut(); + } + + toggleShowHidden() { + this.showHiddenElements = !this.showHiddenElements; + this.showHiddenElementsChange.emit(this.showHiddenElements); + this.scadaSymbolEditObject.showHiddenElements(this.showHiddenElements); + } + + private updateContent(content: string) { + this.displayShowHidden = false; + 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 new file mode 100644 index 0000000000..f3caf1dbfc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts @@ -0,0 +1,1341 @@ +/// +/// 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 { ImageResourceInfo } from '@shared/models/resource.models'; +import * as svgjs from '@svgdotjs/svg.js'; +import { Box, Element, Rect, Style, SVG, Svg, Timeline } from '@svgdotjs/svg.js'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ViewContainerRef } from '@angular/core'; +import { forkJoin, from } from 'rxjs'; +import { + setupAddTagPanelTooltip, + setupTagPanelTooltip +} from '@home/pages/scada-symbol/scada-symbol-tooltip.components'; +import { + ScadaSymbolBehavior, + ScadaSymbolBehaviorType, + scadaSymbolContentData, + ScadaSymbolMetadata, + ScadaSymbolProperty, + ScadaSymbolPropertyType +} from '@home/components/widget/lib/scada/scada-symbol.models'; +import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import { TbHighlightRule } from '@shared/models/ace/ace.models'; +import { ValueType } from '@shared/models/constants'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; +import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper; +import ITooltipPosition = JQueryTooltipster.ITooltipPosition; + +export interface ScadaSymbolData { + imageResource: ImageResourceInfo; + scadaSymbolContent: string; +} + +export interface ScadaSymbolEditObjectCallbacks { + tagHasStateRenderFunction: (tag: string) => boolean; + tagHasClickAction: (tag: string) => boolean; + editTagStateRenderFunction: (tag: string) => void; + editTagClickAction: (tag: string) => void; + tagsUpdated: (tags: string[]) => void; + hasHiddenElements?: (hasHidden: boolean) => void; + onSymbolEditObjectDirty: (dirty: boolean) => void; + onZoom?: () => void; +} + +const minSymbolZoom = 0.75; +const maxSymbolZoom = 4; + +export class ScadaSymbolEditObject { + + public svgShape: Svg; + private svgRootNodePart: string; + private box: Box; + private elements: ScadaSymbolElement[] = []; + private readonly shapeResize$: ResizeObserver; + private performSetup = false; + private hoverFilterStyle: Style; + private showHidden = false; + public scale = 1; + + public tags: string[] = []; + + private zoomFactor = 0.34; + + constructor(private rootElement: HTMLElement, + public tooltipsContainer: HTMLElement, + public viewContainerRef: ViewContainerRef, + private callbacks: ScadaSymbolEditObjectCallbacks, + public readonly: boolean) { + this.shapeResize$ = new ResizeObserver(() => { + this.resize(); + }); + } + + public setReadOnly(readonly: boolean) { + this.readonly = readonly; + } + + public setContent(svgContent: string) { + this.shapeResize$.unobserve(this.rootElement); + if (this.svgShape) { + this.destroyElements(); + this.svgShape.remove(); + } + this.scale = 1; + this.showHidden = false; + const contentData = scadaSymbolContentData(svgContent); + this.svgRootNodePart = contentData.svgRootNode; + this.svgShape = SVG().svg(contentData.innerSvg); + this.svgShape.node.style.overflow = 'visible'; + this.svgShape.node.style['user-select'] = 'none'; + const origSvg = SVG(`${contentData.svgRootNode}\n${contentData.innerSvg}\n`); + if (origSvg.type === 'svg') { + this.box = (origSvg as Svg).viewbox(); + if (origSvg.fill()) { + this.svgShape.fill(origSvg.fill()); + } + } else { + this.box = this.svgShape.bbox(); + } + origSvg.remove(); + this.svgShape.size(this.box.width, this.box.height); + this.svgShape.viewbox(`0 0 ${this.box.width} ${this.box.height}`); + this.svgShape.style().attr('tb:inner', true).rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'}); + this.svgShape.addTo(this.rootElement); + this.updateHoverFilterStyle(); + this.performSetup = true; + this.shapeResize$.observe(this.rootElement); + } + + public getContent(): string { + if (this.svgShape) { + this.elements.forEach(e => e.restoreOrigVisibility()); + const svgContent = this.svgShape.svg((e: Element) => { + if (e.node.hasAttribute('tb:inner')) { + return false; + } else { + e.node.classList.remove('tb-element', 'tooltipstered'); + if (!e.node.classList.length) { + e.node.removeAttribute('class'); + } + e.attr('svgjs:data', null); + } + }, false); + this.showHiddenElements(this.showHidden); + return `${this.svgRootNodePart}\n${svgContent}\n`; + } else { + return null; + } + } + + public cancelEdit() { + 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); + } + + public showHiddenElements(show: boolean) { + this.showHidden = show; + this.elements.forEach(e => show ? e.showInvisible() : e.hideInvisible()); + } + + 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()); + 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(); + (window as any).SVG = svgjs; + forkJoin([ + from(import('tooltipster')), + from(import('tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js')) + ]).subscribe(() => { + this.setupElements(); + }); + } + + private setupZoomPan() { + this.svgShape.panZoom({ + zoomMin: minSymbolZoom, + zoomMax: maxSymbolZoom, + zoomFactor: this.zoomFactor + }); + this.svgShape.on('zoom', (e) => { + const { + detail: { level, focus } + } = e as any; + this.svgShape.zoom(level, focus); + this.postZoom(this.callbacks.onZoom, e); + }); + this.svgShape.on('panning', (e) => { + const box = (e as any).detail.box; + this.svgShape.viewbox(this.restrictToMargins(box)); + e.preventDefault(); + }); + this.svgShape.on('panStart', (_e) => { + this.hideTooltips(); + this.svgShape.node.style.cursor = 'grab'; + }); + this.svgShape.on('panEnd', (_e) => { + this.restoreTooltips(); + this.svgShape.node.style.cursor = 'default'; + }); + this.svgShape.zoom(minSymbolZoom); + } + + private postZoom(callback?: () => void, e?: any) { + const box = this.restrictToMargins(this.svgShape.viewbox()); + this.svgShape.viewbox(box); + setTimeout(() => { + this.updateTooltipPositions(); + }); + e?.preventDefault(); + if (callback) { + callback(); + } + } + + private restrictToMargins(box: Box): Box { + const marginX = Math.max(box.width - this.box.width, 0); + const marginY = Math.max(box.height - this.box.height, 0); + if (box.x < -marginX) { + box.x = -marginX; + } else if ((box.x + box.width) > (this.box.width + marginX)) { + box.x = this.box.width + marginX - box.width; + } + if (box.y < -marginY) { + box.y = -marginY; + } else if ((box.y + box.height) > (this.box.height + marginY)) { + box.y = this.box.height + marginY - box.height; + } + return box; + } + + private setupElements() { + this.svgShape.children().forEach(child => { + this.addElement(child); + }); + const overlappingGroups: ScadaSymbolElement[][] = []; + for (const el of this.elements) { + for (const other of this.elements) { + if (el !== other && el.overlappingCenters(other)) { + let overlappingGroup: ScadaSymbolElement[]; + for (const list of overlappingGroups) { + if (list.includes(other) || list.includes(el)) { + overlappingGroup = list; + break; + } + } + if (!overlappingGroup) { + overlappingGroup = [el, other]; + overlappingGroups.push(overlappingGroup); + } else { + if (!overlappingGroup.includes(el)) { + overlappingGroup.push(el); + } else if (!overlappingGroup.includes(other)){ + overlappingGroup.push(other); + } + } + } + } + } + for (const group of overlappingGroups) { + const centers = group.map(e => e.box.cy); + const center = centers.reduce((a, b) => a + b, 0) / centers.length; + const textElement = group.find(e => e.isText()); + const slots = textElement ? group.length % 2 === 0 ? (group.length + 1) : group.length : group.length; + if (textElement) { + textElement.setInnerTooltipOffset(0, center); + group.splice(group.indexOf(textElement), 1); + } + let offset = - (elementTooltipMinHeight * slots) / 2 + elementTooltipMinHeight / 2; + for (const element of group) { + if (textElement && offset === 0) { + offset += elementTooltipMinHeight; + } + element.setInnerTooltipOffset(offset, center); + offset += elementTooltipMinHeight; + } + } + for (const el of this.elements) { + el.init(); + } + this.updateTags(); + const hasHidden = this.elements.some(e => e.invisible); + if (this.callbacks.hasHiddenElements) { + this.callbacks.hasHiddenElements(hasHidden); + } + } + + private addElement(e: Element, parentInvisible = false) { + if (hasBBox(e)) { + const invisible = parentInvisible || !e.visible(); + const scadaSymbolElement = new ScadaSymbolElement(this, e, invisible); + this.elements.push(scadaSymbolElement); + e.children().forEach(child => { + if (!(child.type === 'tspan' && e.type === 'text')) { + this.addElement(child, invisible); + } + }, true); + } + } + + public destroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + this.destroyElements(); + } + + private destroyElements() { + this.elements.forEach(e => { + e.destroy(); + }); + this.elements.length = 0; + } + + private resize() { + if (this.svgShape) { + const targetWidth = this.rootElement.getBoundingClientRect().width; + const targetHeight = this.rootElement.getBoundingClientRect().height; + if (targetWidth && targetHeight) { + const svgAspect = this.box.width / this.box.height; + const shapeAspect = targetWidth / targetHeight; + let scale: number; + if (svgAspect > shapeAspect) { + scale = targetWidth / this.box.width; + } else { + scale = targetHeight / this.box.height; + } + if (this.scale !== scale) { + this.scale = scale; + this.svgShape.node.style.transform = `scale(${this.scale})`; + this.updateHoverFilterStyle(); + this.updateTooltipPositions(); + } + if (this.performSetup) { + this.performSetup = false; + this.doSetup(); + } + } + } + } + + private updateHoverFilterStyle() { + if (this.hoverFilterStyle) { + this.hoverFilterStyle.remove(); + } + const whiteBlur = (2.8 / this.scale).toFixed(2); + const blackBlur = (1.2 / this.scale).toFixed(2); + this.hoverFilterStyle = + this.svgShape.style().attr('tb:inner', true).rule('.hovered', + { + filter: + `drop-shadow(0px 0px ${whiteBlur}px white) drop-shadow(0px 0px ${whiteBlur}px white) + drop-shadow(0px 0px ${whiteBlur}px white) drop-shadow(0px 0px ${whiteBlur}px white) + drop-shadow(0px 0px ${blackBlur}px black)` + } + ); + } + + private updateTooltipPositions() { + for (const e of this.elements) { + e.updateTooltipPosition(); + } + } + + private hideTooltips() { + for (const e of this.elements) { + e.hideTooltip(); + } + } + + private restoreTooltips() { + for (const e of this.elements) { + e.restoreTooltip(); + } + } + + public updateTags() { + this.tags = this.elements + .filter(e => e.hasTag()) + .map(e => e.tag) + .filter((v, i, a) => a.indexOf(v) === i) + .sort(); + this.callbacks.tagsUpdated(this.tags); + } + + public tagHasStateRenderFunction(tag: string): boolean { + return this.callbacks.tagHasStateRenderFunction(tag); + } + + public tagHasClickAction(tag: string): boolean { + return this.callbacks.tagHasClickAction(tag); + } + + public editTagStateRenderFunction(tag: string) { + this.callbacks.editTagStateRenderFunction(tag); + } + + public editTagClickAction(tag: string) { + this.callbacks.editTagClickAction(tag); + } + + public setDirty(dirty: boolean) { + this.callbacks.onSymbolEditObjectDirty(dirty); + } + +} + +const hasBBox = (e: Element): boolean => { + try { + if (e.bbox) { + let box = e.bbox(); + if (!box.width && !box.height && !e.visible()) { + e.show(); + box = e.bbox(); + e.hide(); + } + return !!box.width || !!box.height; + } else { + return false; + } + } catch (_e) { + return false; + } +}; + +const elementTooltipMinHeight = 36 + 8; +const elementTooltipMinWidth = 100; + +const groupRectStroke = 2; +const groupRectPadding = 2; + +export class ScadaSymbolElement { + + private highlightRect: Rect; + private highlightRectTimeline: Timeline; + + public tooltip: ITooltipsterInstance; + + public tag: string; + + private editing = false; + private onCancelEdit: () => void; + + private innerTooltipOffset = 0; + + public readonly box: Box; + + private highlighted = false; + + private tooltipMouseX: number; + private tooltipMouseY: number; + + private origVisibility = true; + + get readonly(): boolean { + return this.editObject.readonly; + } + + constructor(private editObject: ScadaSymbolEditObject, + public element: Element, + public invisible = false) { + this.tag = element.attr('tb:tag'); + this.origVisibility = element.visible(); + if (this.invisible) { + element.show(); + } + this.box = element.rbox(this.editObject.svgShape); +/* if (element.visible()) { + this.box = element.rbox(this.editObject.svgShape); + if (parentGroup && parentGroup.invisible) { + this.invisible = true; + } + } else { + element.show(); + this.box = element.rbox(this.editObject.svgShape); + element.hide(); + this.invisible = true; + }*/ + } + + public init() { + if (this.isGroup()) { + this.highlightRect = + this.editObject.svgShape + .rect(this.box.width + groupRectPadding * 2, this.box.height + groupRectPadding * 2) + .x(this.box.x - groupRectPadding) + .y(this.box.y - groupRectPadding) + .attr({ + 'tb:inner': true, + fill: 'none', + rx: this.unscaled(6), + stroke: 'rgba(0, 0, 0, 0.38)', + 'stroke-dasharray': '1', + 'stroke-width': this.unscaled(groupRectStroke), + opacity: 0}); + this.highlightRectTimeline = new Timeline(); + this.highlightRect.timeline(this.highlightRectTimeline); + this.highlightRect.hide(); + } else { + this.element.addClass('tb-element'); + } + this.element.on('mouseenter', (_event) => { + this.highlight(); + }); + this.element.on('mouseleave', (_event) => { + this.unhighlight(); + }); + if (!this.invisible) { + this.setupTooltips(); + } + this.hideInvisible(); + } + + private setupTooltips() { + if (this.hasTag()) { + this.createTagTooltip(); + } else if (!this.readonly) { + this.createAddTagTooltip(); + } + } + + public showInvisible() { + if (this.invisible) { + this.element.show(); + if (!this.tooltip) { + this.setupTooltips(); + } + } + } + + public hideInvisible() { + if (this.invisible) { + this.element.hide(); + if (this.tooltip) { + this.tooltip.destroy(); + this.tooltip = null; + } + } + } + + public restoreOrigVisibility() { + if (this.origVisibility) { + this.element.show(); + } else { + this.element.hide(); + } + } + + public overlappingCenters(otherElement: ScadaSymbolElement): boolean { + if (this.isGroup() || otherElement.isGroup()) { + return false; + } + return Math.abs(this.box.cx - otherElement.box.cx) * this.editObject.scale < elementTooltipMinWidth && + Math.abs(this.box.cy - otherElement.box.cy) * this.editObject.scale < elementTooltipMinHeight; + } + + public highlight() { + if (!this.highlighted) { + this.highlighted = true; + if (this.isGroup()) { + this.highlightRectTimeline.finish(); + this.highlightRect + .attr({ + rx: this.unscaled(6), + 'stroke-width': this.unscaled(groupRectStroke) + }); + this.highlightRect.show(); + this.highlightRect.animate(300).attr({opacity: 1}); + } else { + this.element.addClass('hovered'); + } + if (this.hasTag()) { + this.tooltip.reposition(); + $(this.tooltip.elementTooltip()).addClass('tb-active'); + } + } + } + + public unhighlight() { + if (this.highlighted) { + this.highlighted = false; + if (this.isGroup()) { + this.highlightRectTimeline.finish(); + this.highlightRect.animate(300).attr({opacity: 0}).after(() => { + this.highlightRect.hide(); + }); + } else { + this.element.removeClass('hovered'); + } + if (this.hasTag() && !this.editing) { + $(this.tooltip.elementTooltip()).removeClass('tb-active'); + } + } + } + + public clearTag() { + this.tooltip.destroy(); + this.tag = null; + this.element.attr('tb:tag', null); + this.unhighlight(); + this.createAddTagTooltip(); + this.editObject.setDirty(true); + this.editObject.updateTags(); + } + + public setTag(tag: string) { + this.tooltip.destroy(); + this.tag = tag; + this.element.attr('tb:tag', tag); + this.createTagTooltip(); + this.editObject.setDirty(true); + this.editObject.updateTags(); + } + + public hasStateRenderFunction(): boolean { + if (this.hasTag()) { + return this.editObject.tagHasStateRenderFunction(this.tag); + } else { + return false; + } + } + + public hasClickAction(): boolean { + if (this.hasTag()) { + return this.editObject.tagHasClickAction(this.tag); + } else { + return false; + } + } + + public editStateRenderFunction() { + if (this.hasTag()) { + this.editObject.editTagStateRenderFunction(this.tag); + } + } + + public editClickAction() { + if (this.hasTag()) { + this.editObject.editTagClickAction(this.tag); + } + } + + public startEdit(onCancelEdit: () => void) { + if (!this.editing) { + this.editObject.cancelEdit(); + this.editing = true; + this.onCancelEdit = onCancelEdit; + } + } + + public stopEdit(cancel = false) { + if (this.editing) { + this.editing = false; + if (cancel && this.onCancelEdit) { + this.onCancelEdit(); + } + this.onCancelEdit = null; + if (this.hasTag() && !this.highlighted) { + $(this.tooltip.elementTooltip()).removeClass('tb-active'); + } + } + } + + public isEditing() { + return this.editing; + } + + public updateTooltipPosition() { + if (this.tooltip && !this.tooltip.status().destroyed) { + this.tooltip.reposition(); + const tooltipElement = this.tooltip.elementTooltip(); + if (tooltipElement) { + 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; + } + + public destroy() { + if (this.tooltip) { + this.tooltip.destroy(); + } + } + + public get tooltipContainer(): JQuery { + return $(this.editObject.tooltipsContainer); + } + + private unscaled(size: number): number { + return size / (this.editObject.scale * this.editObject.svgShape.zoom()); + } + + private scaled(size: number): number { + return size * (this.editObject.scale * this.editObject.svgShape.zoom()); + } + + private createTagTooltip() { + const el = $(this.element.node); + el.tooltipster( + { + parent: this.tooltipContainer, + zIndex: 100, + arrow: this.isGroup(), + distance: this.isGroup() ? (this.scaled(groupRectPadding) + groupRectStroke) : 6, + theme: ['scada-symbol'], + delay: 0, + animationDuration: 0, + interactive: true, + trigger: 'custom', + side: 'top', + trackOrigin: false, + content: '', + functionPosition: (instance, helper, position) => + this.innerTagTooltipPosition(instance, helper, position), + functionReady: (_instance, helper) => { + const tooltipEl = $(helper.tooltip); + tooltipEl.on('mouseenter', () => { + this.highlight(); + }); + tooltipEl.on('mouseleave', () => { + this.unhighlight(); + }); + } + } + ); + this.tooltip = el.tooltipster('instance'); + this.setupTagPanel(); + } + + private setupTagPanel() { + setupTagPanelTooltip(this, this.editObject.viewContainerRef); + } + + private createAddTagTooltip() { + const el = $(this.element.node); + el.tooltipster( + { + parent: this.tooltipContainer, + zIndex: 100, + arrow: true, + theme: ['scada-symbol', 'tb-active'], + delay: [0, 300], + interactive: true, + trigger: 'hover', + side: ['top', 'left', 'bottom', 'right'], + trackOrigin: false, + content: '', + functionBefore: (instance, helper) => { + const mouseEvent = (helper.event as MouseEvent); + this.tooltipMouseX = mouseEvent.clientX; + this.tooltipMouseY = mouseEvent.clientY; + let side: TooltipPositioningSide; + if (this.isGroup()) { + side = 'top'; + } else { + side = this.calculateTooltipSide(helper.origin.getBoundingClientRect(), + mouseEvent.clientX, mouseEvent.clientY); + } + instance.option('side', side); + }, + functionPosition: (instance, helper, position) => + this.innerAddTagTooltipPosition(instance, helper, position), + functionReady: (_instance, helper) => { + const tooltipEl = $(helper.tooltip); + tooltipEl.on('mouseenter', () => { + this.highlight(); + }); + tooltipEl.on('mouseleave', () => { + this.unhighlight(); + }); + } + } + ); + this.tooltip = el.tooltipster('instance'); + this.setupAddTagPanel(); + } + + private setupAddTagPanel() { + setupAddTagPanelTooltip(this, this.editObject.viewContainerRef); + } + + private innerTagTooltipPosition(_instance: ITooltipsterInstance, helper: ITooltipsterHelper, + position: ITooltipPosition): ITooltipPosition { + const clientRect = helper.origin.getBoundingClientRect(); + if (!this.isGroup()) { + position.coord.top = clientRect.top + (clientRect.height - position.size.height) / 2 + + this.innerTooltipOffset * this.editObject.svgShape.zoom(); + position.coord.left = clientRect.left + (clientRect.width - position.size.width) / 2; + } else { + position.distance = this.scaled(groupRectPadding) + groupRectStroke; + position.coord.top = clientRect.top - position.size.height - (this.scaled(groupRectPadding) + groupRectStroke); + } + return position; + } + + private innerAddTagTooltipPosition(_instance: ITooltipsterInstance, + _helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition { + const distance = 10; + switch (position.side) { + case 'right': + position.coord.top = this.tooltipMouseY - position.size.height / 2; + position.coord.left = this.tooltipMouseX + distance; + position.target = this.tooltipMouseY; + break; + case 'top': + position.coord.top = this.tooltipMouseY - position.size.height - distance; + position.coord.left = this.tooltipMouseX - position.size.width / 2; + position.target = this.tooltipMouseX; + if (this.isGroup()) { + position.coord.top -= elementTooltipMinHeight; + } + break; + case 'left': + position.coord.top = this.tooltipMouseY - position.size.height / 2; + position.coord.left = this.tooltipMouseX - position.size.width - distance; + position.target = this.tooltipMouseY; + break; + case 'bottom': + position.coord.top = this.tooltipMouseY + distance; + position.coord.left = this.tooltipMouseX - position.size.width / 2; + position.target = this.tooltipMouseX; + break; + } + return position; + } + + private calculateTooltipSide(clientRect: DOMRect, mouseX: number, mouseY: number): TooltipPositioningSide { + let side: TooltipPositioningSide; + const cx = clientRect.left + clientRect.width / 2; + const cy = clientRect.top + clientRect.height / 2; + if (clientRect.width > clientRect.height) { + if (Math.abs(cx - mouseX) > clientRect.width / 4) { + if (mouseX < cx) { + side = 'left'; + } else { + side = 'right'; + } + } else { + if (mouseY < cy) { + side = 'top'; + } else { + side = 'bottom'; + } + } + } else { + if (Math.abs(cy - mouseY) > clientRect.height / 4) { + if (mouseY < cy) { + side = 'top'; + } else { + side = 'bottom'; + } + } else { + if (mouseX < cx) { + side = 'left'; + } else { + side = 'right'; + } + } + } + return side; + } + + public hasTag() { + return !!this.tag; + } + + public isGroup() { + return this.element.type === 'g'; + } + + public isText() { + return this.element.type === 'text'; + } + +} + +const scadaSymbolCtxObjectHighlightRules: TbHighlightRule[] = [ + { + class: 'scada-symbol-ctx', + regex: /(?<=\W|^)(ctx)(?=\W|$)\b/ + } +]; + +export const scadaSymbolGeneralStateRenderHighlightRules: TbHighlightRule[] = + scadaSymbolCtxObjectHighlightRules.concat({ + class: 'scada-symbol-svg', + regex: /(?<=\W|^)(svg)(?=\W|$)\b/ + }); + +export const scadaSymbolElementStateRenderHighlightRules: TbHighlightRule[] = + scadaSymbolCtxObjectHighlightRules.concat({ + class: 'scada-symbol-element', + regex: /(?<=\W|^)(element)(?=\W|$)\b/ + }); + +export const scadaSymbolClickActionHighlightRules: TbHighlightRule[] = + scadaSymbolElementStateRenderHighlightRules.concat({ + class: 'scada-symbol-event', + regex: /(?<=\W|^)(event)(?=\W|$)\b/ + }); + +const scadaSymbolCtxPropertyHighlightRules: TbHighlightRule[] = [ + { + class: 'scada-symbol-ctx-properties', + regex: /(?<=ctx\.)(properties)\b/ + }, + { + class: 'scada-symbol-ctx-tags', + regex: /(?<=ctx\.)(tags)\b/ + }, + { + class: 'scada-symbol-ctx-values', + regex: /(?<=ctx\.)(values)\b/ + }, + { + class: 'scada-symbol-ctx-api', + regex: /(?<=ctx\.)(api)\b/ + }, + { + class: 'scada-symbol-ctx-svg', + regex: /(?<=ctx\.)(svg)\b/ + }, + { + class: 'scada-symbol-ctx-property', + regex: /(?<=ctx\.properties\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/ + }, + { + class: 'scada-symbol-ctx-tag', + regex: /(?<=ctx\.tags\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/ + }, + { + class: 'scada-symbol-ctx-value', + regex: /(?<=ctx\.values\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/ + }, + { + class: 'scada-symbol-ctx-api-method', + regex: /(?<=ctx\.api\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/ + }, + { + class: 'scada-symbol-ctx-svg-method', + regex: /(?<=ctx\.svg\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/ + } +]; + +export const scadaSymbolGeneralStateRenderPropertiesHighlightRules: TbHighlightRule[] = + scadaSymbolCtxPropertyHighlightRules.concat({ + class: 'scada-symbol-svg-properties', + regex: /(?<=svg\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/ + }); + +export const scadaSymbolElementStateRenderPropertiesHighlightRules: TbHighlightRule[] = + scadaSymbolCtxPropertyHighlightRules.concat({ + class: 'scada-symbol-element-properties', + regex: /(?<=element\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/ + }); + +export const scadaSymbolClickActionPropertiesHighlightRules: TbHighlightRule[] = + scadaSymbolElementStateRenderPropertiesHighlightRules.concat({ + class: 'scada-symbol-event-properties', + regex: /(?<=event\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/ + }); + +export const generalStateRenderFunctionCompletions = (ctxCompletion: TbEditorCompletion): TbEditorCompletions => ({ + ctx: ctxCompletion, + svg: { + meta: 'argument', + type: 'SVG.Svg', + description: 'A root svg node. Instance of SVG.Svg.' + } + }); + +export const elementStateRenderFunctionCompletions = (ctxCompletion: TbEditorCompletion): TbEditorCompletions => ({ + ctx: ctxCompletion, + element: { + meta: 'argument', + type: 'Element', + description: 'SVG element.
' + + 'See Manipulating section to manipulate the element.
' + + 'See Animating section to animate the element.' + } + }); + +export const clickActionFunctionCompletions = (ctxCompletion: TbEditorCompletion): TbEditorCompletions => { + const completions = elementStateRenderFunctionCompletions(ctxCompletion); + completions.event = { + meta: 'argument', + type: 'Event', + description: 'DOM event.' + }; + return completions; +}; + +export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags: string[], + customTranslate: CustomTranslatePipe): TbEditorCompletion => { + const properties: TbEditorCompletion = { + meta: 'object', + type: 'object', + description: 'An object holding all defined SCADA symbol properties.', + children: {} + }; + for (const property of metadata.properties) { + properties.children[property.id] = scadaSymbolPropertyCompletion(property, customTranslate); + } + const values: TbEditorCompletion = { + meta: 'object', + type: 'object', + description: 'An object holding all values obtained using behaviors of type "Value"', + children: {} + }; + const getValues = metadata.behavior.filter(b => b.type === ScadaSymbolBehaviorType.value); + for (const value of getValues) { + values.children[value.id] = scadaSymbolValueCompletion(value, customTranslate); + } + const tagsCompletions: TbEditorCompletion = { + meta: 'object', + type: 'object', + description: 'An object holding all tagged SVG elements grouped by tags (object keys).', + children: {} + }; + if (tags?.length) { + for (const tag of tags) { + tagsCompletions.children[tag] = { + meta: 'property', + description: 'An array of SVG elements.', + type: 'Array' + }; + } + } + return { + meta: 'argument', + type: 'ScadaSymbolContext', + description: 'Context of the SCADA symbol.', + children: { + api: { + meta: 'object', + type: 'ScadaSymbolApi', + description: 'SCADA symbol API', + children: { + animate: { + meta: 'function', + description: 'Finishes any previous animation and starts new animation for SVG element.', + args: [ + { + name: 'element', + description: 'SVG element', + type: 'Element' + }, + { + name: 'duration', + description: 'Animation duration in milliseconds', + type: 'number' + } + ], + return: { + description: 'Instance of SVG.Runner which has the same methods as any element and additional methods to control the runner.', + type: 'SVG.Runner' + } + }, + resetAnimation: { + meta: 'function', + description: 'Stops animation if any and restore SVG element initial state, resets animation timeline.', + args: [ + { + name: 'element', + description: 'SVG element', + type: 'Element' + }, + ] + }, + finishAnimation: { + meta: 'function', + description: 'Finishes animation if any, SVG element state updated according to the end animation values, ' + + 'resets animation timeline.', + args: [ + { + name: 'element', + description: 'SVG element', + type: 'Element' + }, + ] + }, + generateElementId: { + meta: 'function', + description: 'Generates new unique element id.', + args: [], + return: { + type: 'string', + description: 'Newly generated element id.' + } + }, + formatValue: { + meta: 'function', + description: 'Formats numeric value according to specified decimals and units', + args: [ + { + name: 'value', + description: 'Numeric value to be formatted', + type: 'any' + }, + { + name: 'dec', + description: 'Number of decimal digits', + type: 'number', + optional: true + }, + { + name: 'units', + description: 'Units to append to the formatted value', + type: 'string', + optional: true + }, + { + name: 'showZeroDecimals', + description: 'Whether to keep zero decimal digits', + type: 'boolean', + optional: true + } + ], + return: { + type: 'string', + description: 'Formatted value' + } + }, + text: { + meta: 'function', + description: 'Set text to element(s). Only applicable for elements of type ' + + 'SVG.Text or ' + + 'SVG.Tspan.', + args: [ + { + name: 'element', + description: 'SVG element or an array of SVG elements', + type: 'Element | Array<Element>' + }, + { + name: 'text', + description: 'Text to be set', + type: 'string' + } + ] + }, + font: { + meta: 'function', + description: 'Set element(s) text font and color. Only applicable for elements of type ' + + 'SVG.Text or ' + + 'SVG.Tspan.', + args: [ + { + name: 'element', + description: 'SVG element or an array of SVG elements', + type: 'Element | Array<Element>' + }, + { + name: 'font', + description: 'Font settings object used to apply text element font', + type: 'Font' + }, + { + name: 'color', + description: 'Color string used to apply text color of the element', + type: 'string' + } + ] + }, + disable: { + meta: 'function', + description: 'Disables element(s). Disabled element doesn\'t accept any user interaction. ' + + 'For ex. if disabled element has click action, no click action will be performed on user click.', + args: [ + { + name: 'element', + description: 'SVG element or an array of SVG elements', + type: 'Element | Array<Element>' + } + ] + }, + enable: { + meta: 'function', + description: 'Enables disabled element(s). Enabled element accepts user interaction. ' + + 'For ex. if element has click action, click action will be performed on user click.', + args: [ + { + name: 'element', + description: 'SVG element or an array of SVG elements', + type: 'Element | Array<Element>' + } + ] + }, + callAction: { + meta: 'function', + description: 'Invokes action specified by behavior of type "Action" found by behaviorId.', + args: [ + { + name: 'event', + description: 'DOM event', + type: 'Event' + }, + { + name: 'behaviorId', + description: 'Id of the "Action" behavior', + type: 'string' + }, + { + name: 'value', + description: 'Optional value passed to behavior', + type: 'any', + optional: true + }, + { + name: 'observer', + description: 'Optional observer callback', + type: 'Partial<Observer<void>>', + optional: true + } + ] + }, + setValue: { + meta: 'function', + description: 'Updates value by valueId. See ctx.values for reference. ' + + 'Value update triggers all render functions.', + args: [ + { + name: 'valueId', + description: 'Id of the value to be updated', + type: 'string' + }, + { + name: 'value', + description: 'New value to be set', + type: 'any' + } + ] + } + } + }, + properties, + values, + tags: tagsCompletions, + svg: { + meta: 'argument', + type: 'SVG.Svg', + description: 'A root svg node. Instance of SVG.Svg.' + } + } + }; +}; + +const scadaSymbolPropertyCompletion = (property: ScadaSymbolProperty, customTranslate: CustomTranslatePipe): TbEditorCompletion => { + let description = customTranslate.transform(property.name, property.name); + if (property.subLabel) { + description += ` ${customTranslate.transform(property.subLabel, property.subLabel)}`; + } + return { + meta: 'property', + description, + type: scadaSymbolPropertyCompletionType(property.type) + }; +}; + +const scadaSymbolValueCompletion = (value: ScadaSymbolBehavior, customTranslate: CustomTranslatePipe): TbEditorCompletion => { + const description = customTranslate.transform(value.name, value.name); + return { + meta: 'property', + description, + type: scadaSymbolValueCompletionType(value.valueType) + }; +}; + +const scadaSymbolPropertyCompletionType = (type: ScadaSymbolPropertyType): string => { + switch (type) { + case ScadaSymbolPropertyType.text: + return 'string'; + case ScadaSymbolPropertyType.number: + return 'number'; + case ScadaSymbolPropertyType.switch: + return 'boolean'; + case ScadaSymbolPropertyType.color: + return 'color string'; + case ScadaSymbolPropertyType.color_settings: + return 'ColorProcessor'; + case ScadaSymbolPropertyType.font: + return 'Font'; + case ScadaSymbolPropertyType.units: + return 'units string'; + } +}; + +const scadaSymbolValueCompletionType = (type: ValueType): string => { + switch (type) { + case ValueType.STRING: + return 'string'; + case ValueType.INTEGER: + case ValueType.DOUBLE: + return 'number'; + case ValueType.BOOLEAN: + return 'boolean'; + case ValueType.JSON: + return 'object'; + } +}; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss new file mode 100644 index 0000000000..43613b6d40 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.component.scss @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-tooltip-panel { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + &.column { + flex-direction: column; + } + &.flex-start { + align-items: start; + } + + .tb-confirm-text { + font-size: 14px; + line-height: 16px; + text-align: center; + font-weight: 400; + } + + .mdc-button { + line-height: 20px; + font-size: 12px; + &.mat-mdc-button-base { + height: 20px; + .mat-mdc-button-touch-target { + height: 20px; + } + } + } + + .mat-mdc-form-field.tb-inline-field { + font-size: 12px; + line-height: 12px; + .mat-mdc-text-field-wrapper { + padding: 0 8px; + .mat-mdc-form-field-flex { + .mat-mdc-form-field-infix { + padding-top: 4px; + padding-bottom: 4px; + min-height: 20px; + .mat-mdc-input-element { + font-size: 12px; + line-height: 12px; + height: 12px; + } + } + } + } + } +} 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 new file mode 100644 index 0000000000..ef811e9746 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -0,0 +1,519 @@ +/// +/// 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 { + AfterViewInit, + Component, + ComponentRef, + Directive, + ElementRef, + EventEmitter, + Input, + NgModule, OnDestroy, + OnInit, + Output, + Type, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { ScadaSymbolElement } from '@home/pages/scada-symbol/scada-symbol-editor.models'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { ENTER } from '@angular/cdk/keycodes'; +import Timeout = NodeJS.Timeout; +import { MatButton } from '@angular/material/button'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import { TranslateService } from '@ngx-translate/core'; + +@Directive() +abstract class ScadaSymbolPanelComponent implements AfterViewInit { + + @Input() + symbolElement: ScadaSymbolElement; + + @Output() + viewInited = new EventEmitter(); + + protected constructor(public element: ElementRef) { + } + + ngAfterViewInit() { + this.viewInited.emit(); + } +} + +@Component({ + template: `
+ {{ symbolElement?.element?.type }}{{ symbolElement?.invisible ? ' (' + ('scada.hidden' | translate) + ')' : '' }} + +
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolAddTagPanelComponent extends ScadaSymbolPanelComponent { + + @Output() + addTag = new EventEmitter(); + + constructor(public element: ElementRef) { + super(element); + } + + public onAddTag() { + this.addTag.emit(); + } + +} + +@Component({ + template: `
+ {{ (isAdd ? 'scada.tag.enter-tag' : 'scada.tag.update-tag' ) | translate }}: + + + + + +
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolTagInputPanelComponent extends ScadaSymbolPanelComponent implements AfterViewInit, OnDestroy { + + @ViewChild('tagField') + tagField: ElementRef; + + @Input() + isAdd: boolean; + + @Input() + tag: string; + + @Output() + apply = new EventEmitter(); + + @Output() + cancel = new EventEmitter(); + + private closed = false; + + private blurTimeout: Timeout; + + constructor(public element: ElementRef) { + super(element); + } + + ngAfterViewInit() { + super.ngAfterViewInit(); + setTimeout(() => { + this.tagField.nativeElement.focus(); + }); + } + + ngOnDestroy() { + if (this.blurTimeout) { + clearTimeout(this.blurTimeout); + this.blurTimeout = null; + } + } + + public tagEnter($event: KeyboardEvent) { + if ($event.keyCode === ENTER) { + $event.preventDefault(); + if (this.tag) { + this.closed = true; + this.apply.emit(this.tag); + } + } + } + + public onApply() { + this.closed = true; + if (this.tag) { + this.apply.emit(this.tag); + } else { + this.cancel.emit(); + } + } + + public onCancel() { + this.closed = true; + this.cancel.emit(); + } + + public onBlur() { + this.blurTimeout = setTimeout(() => { + if (!this.closed) { + this.closed = true; + this.cancel.emit(); + } + }, 300); + } + +} + +@Component({ + template: `
+ {{ symbolElement?.element?.type }}{{ symbolElement?.invisible ? ' (' + ('scada.hidden' | translate) + ')' : '' }}: + {{ symbolElement?.tag }} + + + +
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { + + @ViewChild('tagSettingsButton', {read: ElementRef}) + tagSettingsButton: ElementRef; + + @ViewChild('removeTagButton', {read: ElementRef}) + removeTagButton: ElementRef; + + @Output() + updateTag = new EventEmitter(); + + @Output() + removeTag = new EventEmitter(); + + displayTagSettings = true; + + constructor(public element: ElementRef, + private container: ViewContainerRef) { + super(element); + } + + ngOnInit() { + if (this.symbolElement?.readonly) { + this.displayTagSettings = (this.symbolElement?.hasStateRenderFunction() || this.symbolElement?.hasClickAction()); + } + } + + ngAfterViewInit() { + super.ngAfterViewInit(); + setTimeout(() => { + + if (this.displayTagSettings) { + const tagSettingsButton = $(this.tagSettingsButton.nativeElement); + tagSettingsButton.tooltipster( + { + parent: this.symbolElement.tooltipContainer, + zIndex: 200, + arrow: true, + theme: ['scada-symbol', 'tb-active'], + interactive: true, + trigger: 'click', + trackOrigin: true, + trackerInterval: 100, + side: 'top', + content: '' + } + ); + + const scadaSymbolTagSettingsTooltip = tagSettingsButton.tooltipster('instance'); + setTooltipComponent(this.symbolElement, this.container, ScadaSymbolTagSettingsComponent, scadaSymbolTagSettingsTooltip); + } + + if (!this.symbolElement.readonly) { + const removeTagButton = $(this.removeTagButton.nativeElement); + removeTagButton.tooltipster( + { + parent: this.symbolElement.tooltipContainer, + zIndex: 200, + arrow: true, + theme: ['scada-symbol', 'tb-active'], + interactive: true, + trigger: 'click', + trackOrigin: true, + trackerInterval: 100, + side: 'top', + content: '' + } + ); + + const scadaSymbolRemoveTagTooltip = removeTagButton.tooltipster('instance'); + const scadaSymbolRemoveTagCompRef = + setTooltipComponent(this.symbolElement, this.container, ScadaSymbolRemoveTagConfirmComponent, scadaSymbolRemoveTagTooltip); + scadaSymbolRemoveTagCompRef.instance.removeTag.subscribe(() => { + scadaSymbolRemoveTagTooltip.destroy(); + this.removeTag.emit(); + }); + scadaSymbolRemoveTagCompRef.instance.cancel.subscribe(() => { + scadaSymbolRemoveTagTooltip.close(); + }); + scadaSymbolRemoveTagTooltip.on('ready', () => { + scadaSymbolRemoveTagCompRef.instance.yesButton.focus(); + }); + } + }); + } + + public onUpdateTag() { + this.updateTag.emit(); + } +} + +@Component({ + template: `
+
+
+ + +
+
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { + + @ViewChild('yesButton') + yesButton: MatButton; + + deleteText: string; + + @Output() + cancel = new EventEmitter(); + + @Output() + removeTag = new EventEmitter(); + + constructor(public element: ElementRef, + private translate: TranslateService) { + super(element); + } + + ngOnInit() { + this.deleteText = this.translate.instant('scada.tag.delete-tag-text', + {tag: this.symbolElement?.tag, elementType: this.symbolElement?.element?.type}); + } + + public onCancel() { + this.cancel.emit(); + } + + public onRemoveTag() { + this.removeTag.emit(); + } +} + +@Component({ + template: `
+
scada.state-render-function
+ + +
scada.tag.on-click-action
+ + +
`, + styleUrls: ['./scada-symbol-tooltip.component.scss'], + encapsulation: ViewEncapsulation.None +}) +class ScadaSymbolTagSettingsComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { + + hasStateRenderFunction = false; + + hasClickAction = false; + + constructor(public element: ElementRef) { + super(element); + } + + ngOnInit() { + this.updateFunctionsState(); + } + + private updateFunctionsState() { + this.hasStateRenderFunction = this.symbolElement.hasStateRenderFunction(); + this.hasClickAction = this.symbolElement.hasClickAction(); + } + + editStateRenderFunction() { + this.symbolElement.editStateRenderFunction(); + } + + editClickAction() { + this.symbolElement.editClickAction(); + } +} + +@NgModule({ + declarations: + [ + ScadaSymbolAddTagPanelComponent, + ScadaSymbolTagInputPanelComponent, + ScadaSymbolTagPanelComponent, + ScadaSymbolRemoveTagConfirmComponent, + ScadaSymbolTagSettingsComponent + ], + imports: [ + CommonModule, + SharedModule + ] +}) +export class ScadaSymbolTooltipComponentsModule { } + +export const setupAddTagPanelTooltip = (symbolElement: ScadaSymbolElement, container: ViewContainerRef) => { + symbolElement.stopEdit(); + symbolElement.tooltip.off('close'); + const componentRef = setTooltipComponent(symbolElement, container, ScadaSymbolAddTagPanelComponent); + componentRef.instance.addTag.subscribe(() => { + componentRef.destroy(); + setupTagInputPanelTooltip(symbolElement, container, true); + }); +}; + +export const setupTagPanelTooltip = (symbolElement: ScadaSymbolElement, container: ViewContainerRef) => { + symbolElement.stopEdit(); + symbolElement.unhighlight(); + const componentRef = setTooltipComponent(symbolElement, container, ScadaSymbolTagPanelComponent); + componentRef.instance.updateTag.subscribe(() => { + componentRef.destroy(); + setupTagInputPanelTooltip(symbolElement, container, false); + }); + componentRef.instance.removeTag.subscribe(() => { + componentRef.destroy(); + symbolElement.clearTag(); + }); + symbolElement.tooltip.open(); +}; + +const setupTagInputPanelTooltip = (symbolElement: ScadaSymbolElement, container: ViewContainerRef, isAdd: boolean) => { + + symbolElement.startEdit(() => { + if (isAdd) { + symbolElement.unhighlight(); + symbolElement.tooltip.close(); + } else { + componentRef.destroy(); + setupTagPanelTooltip(symbolElement, container); + } + }); + + const componentRef = setTooltipComponent(symbolElement, container, ScadaSymbolTagInputPanelComponent); + + componentRef.instance.isAdd = isAdd; + if (!isAdd) { + componentRef.instance.tag = symbolElement.tag; + } + componentRef.instance.apply.subscribe((newTag) => { + componentRef.destroy(); + if (isAdd) { + symbolElement.tooltip.off('close'); + } + symbolElement.setTag(newTag); + }); + componentRef.instance.cancel.subscribe(() => { + symbolElement.stopEdit(true); + }); + if (isAdd) { + symbolElement.tooltip.option('delay', [0, 10000000]); + symbolElement.tooltip.on('close', () => { + componentRef.destroy(); + symbolElement.tooltip.option('delay', [0, 300]); + setupAddTagPanelTooltip(symbolElement, container); + }); + } +}; + +const setTooltipComponent = (symbolElement: ScadaSymbolElement, + container: ViewContainerRef, + componentType: Type, + tooltip?: ITooltipsterInstance): ComponentRef => { + if (!tooltip) { + tooltip = symbolElement.tooltip; + } + const componentRef = container.createComponent(componentType); + componentRef.instance.symbolElement = symbolElement; + componentRef.instance.viewInited.subscribe(() => { + if (tooltip.status().open) { + tooltip.reposition(); + } + }); + tooltip.on('destroyed', () => { + componentRef.destroy(); + }); + const parentElement = componentRef.instance.element.nativeElement; + const content = parentElement.firstChild; + parentElement.removeChild(content); + parentElement.style.display = 'none'; + tooltip.content(content); + return componentRef; +}; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html new file mode 100644 index 0000000000..6662509fa6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html @@ -0,0 +1,151 @@ + +
+
+
+ + + +
+ +
+
+
+ + + + +
+
+
+
+
scada.symbol.symbol
+ + +
+ + +
+
+
+
+ + + + +
+
+
+ + + + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss new file mode 100644 index 0000000000..cbfe6e6846 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss @@ -0,0 +1,74 @@ +/** + * 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 '../../../../../scss/constants'; + +.tb-scada-symbol-editor { + width: 100%; + height: 100%; + .tb-scada-symbol-editor-details-drawer { + width: 50%; + .tb-scada-symbol-editor-preview-content { + display: flex; + flex-direction: column; + gap: 8px; + .tb-scada-symbol-editor-preview-header { + padding: 24px 24px 0; + display: flex; + gap: 12px; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + .tb-scada-symbol-editor-preview-settings { + & > .mat-content { + padding-top: 8px; + @media #{$mat-xs} { + padding-left: 8px; + padding-right: 8px; + } + } + flex: 1; + overflow: auto; + & > div { + padding: 16px; + } + .mat-content { + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + gap: 8px; + } + } + } + } + } + .tb-scada-symbol-editor-content { + flex: 1; + min-width: 0; + min-height: 0; + max-width: 50%; + background: #fff; + &.preview { + #gridster-parent { + #gridster-background { + background-color: #eee; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts new file mode 100644 index 0000000000..a7c6eee118 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -0,0 +1,477 @@ +/// +/// 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 { + AfterViewInit, + ChangeDetectorRef, + Component, + EventEmitter, + HostBinding, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActivatedRoute } from '@angular/router'; +import { map, switchMap, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { ScadaSymbolData, ScadaSymbolEditObjectCallbacks } from '@home/pages/scada-symbol/scada-symbol-editor.models'; +import { + parseScadaSymbolMetadataFromContent, + ScadaSymbolMetadata, + ScadaSymbolObjectSettings, + updateScadaSymbolMetadataInContent +} from '@home/components/widget/lib/scada/scada-symbol.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { createFileFromContent, deepClone } from '@core/utils'; +import { + ScadaSymbolEditorComponent, + ScadaSymbolEditorData +} from '@home/pages/scada-symbol/scada-symbol-editor.component'; +import { ImageService } from '@core/http/image.service'; +import { imageResourceType, IMAGES_URL_PREFIX, TB_IMAGE_PREFIX } from '@shared/models/resource.models'; +import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; +import { IAliasController, IStateController, StateParams } from '@core/api/widget-api.models'; +import { EntityAliases } from '@shared/models/alias.models'; +import { Filters } from '@shared/models/query/query.models'; +import { AliasController } from '@core/api/alias-controller'; +import { EntityService } from '@core/http/entity.service'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Widget, WidgetConfig, widgetType, WidgetTypeDetails } from '@shared/models/widget.models'; +import { + scadaSymbolWidgetDefaultSettings, + ScadaSymbolWidgetSettings +} from '@home/components/widget/lib/scada/scada-symbol-widget.models'; +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; +import { + ScadaSymbolMetadataComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { + UploadImageDialogComponent, + UploadImageDialogData, UploadImageDialogResult +} from '@shared/components/image/upload-image-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { BackgroundType, colorBackground } from '@shared/models/widget-settings.models'; +import { GridType } from 'angular-gridster2'; +import { + SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogData, + SaveWidgetTypeAsDialogResult +} from '@home/pages/widget/save-widget-type-as-dialog.component'; +import { WidgetService } from '@core/http/widget.service'; +import { de } from 'date-fns/locale'; + +@Component({ + selector: 'tb-scada-symbol', + templateUrl: './scada-symbol.component.html', + styleUrls: ['./scada-symbol.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolComponent extends PageComponent + implements OnInit, OnDestroy, AfterViewInit, HasDirtyFlag, ScadaSymbolEditObjectCallbacks { + + widgetType = widgetType; + + GridType = GridType; + + @HostBinding('style.width') width = '100%'; + @HostBinding('style.height') height = '100%'; + + @ViewChild('symbolEditor', {static: false}) + symbolEditor: ScadaSymbolEditorComponent; + + @ViewChild('symbolMetadata') + symbolMetadata: ScadaSymbolMetadataComponent; + + symbolData: ScadaSymbolData; + symbolEditorData: ScadaSymbolEditorData; + + previewMode = false; + previewMetadata: ScadaSymbolMetadata; + + scadaSymbolFormGroup: UntypedFormGroup; + + scadaPreviewFormGroup: UntypedFormGroup; + + private destroy$ = new Subject(); + + private origSymbolData: ScadaSymbolData; + + updateBreadcrumbs = new EventEmitter(); + + aliasController: IAliasController; + + widgetActionCallbacks: WidgetActionCallbacks = { + fetchDashboardStates: () => [], + fetchCellClickColumns: () => [] + }; + + previewWidget: Widget; + + previewWidgets: Array = []; + + tags: string[]; + + editObjectCallbacks: ScadaSymbolEditObjectCallbacks = this; + + symbolEditorDirty = false; + + private previewScadaSymbolObjectSettings: ScadaSymbolObjectSettings; + + private forcePristine = false; + + private authUser = getCurrentAuthUser(this.store); + + readonly: boolean; + + showHiddenElements = false; + + get isDirty(): boolean { + return (this.scadaSymbolFormGroup.dirty || this.symbolEditorDirty) && !this.forcePristine; + } + + set isDirty(value: boolean) { + this.forcePristine = !value; + } + + constructor(protected store: Store, + private route: ActivatedRoute, + private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private entityService: EntityService, + private utils: UtilsService, + private translate: TranslateService, + private imageService: ImageService, + private widgetService: WidgetService, + private dialog: MatDialog) { + super(store); + } + + ngOnInit(): void { + this.scadaSymbolFormGroup = this.fb.group({ + metadata: [null] + }); + this.scadaPreviewFormGroup = this.fb.group({ + scadaSymbolObjectSettings: [null] + }); + + const entitiAliases: EntityAliases = {}; + + // @ts-ignore + const stateController: IStateController = { + getStateParams: (): StateParams => ({}) + }; + + const filters: Filters = {}; + + this.aliasController = new AliasController(this.utils, + this.entityService, + this.translate, + () => stateController, entitiAliases, filters); + + this.route.data.pipe( + takeUntil(this.destroy$) + ).subscribe( + () => { + this.reset(); + this.init(this.route.snapshot.data.symbolData); + } + ); + } + + ngAfterViewInit() { + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + onApplyScadaSymbolConfig() { + if (this.scadaSymbolFormGroup.valid) { + const metadata: ScadaSymbolMetadata = this.scadaSymbolFormGroup.get('metadata').value; + const scadaSymbolContent = this.prepareScadaSymbolContent(metadata); + const file = createFileFromContent(scadaSymbolContent, this.symbolData.imageResource.fileName, + this.symbolData.imageResource.descriptor.mediaType); + const type = imageResourceType(this.symbolData.imageResource); + let imageInfoObservable = + this.imageService.updateImage(type, this.symbolData.imageResource.resourceKey, file); + if (metadata.title !== this.symbolData.imageResource.title) { + imageInfoObservable = imageInfoObservable.pipe( + switchMap(imageInfo => { + imageInfo.title = metadata.title; + return this.imageService.updateImageInfo(imageInfo); + }) + ); + } + imageInfoObservable.pipe( + switchMap(imageInfo => this.imageService.getImageString( + `${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(imageInfo.resourceKey)}`).pipe( + map(content => ({ + imageResource: imageInfo, + scadaSymbolContent: content + })) + )) + ).subscribe(data => { + this.init(data); + this.updateBreadcrumbs.emit(); + }); + } + } + + onRevertScadaSymbolConfig() { + this.init(this.origSymbolData); + } + + enterPreviewMode() { + this.previewMetadata = this.scadaSymbolFormGroup.get('metadata').value; + this.symbolData.scadaSymbolContent = this.prepareScadaSymbolContent(this.previewMetadata); + this.previewScadaSymbolObjectSettings = { + behavior: {}, + properties: {} + }; + this.scadaPreviewFormGroup.patchValue({ + scadaSymbolObjectSettings: this.previewScadaSymbolObjectSettings + }, {emitEvent: false}); + this.scadaPreviewFormGroup.markAsPristine(); + const settings: ScadaSymbolWidgetSettings = {...scadaSymbolWidgetDefaultSettings, + ...{ + simulated: true, + scadaSymbolUrl: null, + scadaSymbolContent: this.symbolData.scadaSymbolContent, + scadaSymbolObjectSettings: this.previewScadaSymbolObjectSettings, + padding: '0', + background: colorBackground('rgba(0,0,0,0)') + } + }; + this.previewWidget = { + typeFullFqn: 'system.scada_symbol', + type: widgetType.rpc, + sizeX: this.previewMetadata.widgetSizeX || 3, + sizeY: this.previewMetadata.widgetSizeY || 3, + row: 0, + col: 0, + config: { + settings, + showTitle: false, + dropShadow: false, + padding: '0', + margin: '0', + backgroundColor: 'rgba(0,0,0,0)' + } + }; + this.previewWidgets = [this.previewWidget]; + this.previewMode = true; + } + + exitPreviewMode() { + this.symbolEditorData = { + scadaSymbolContent: this.symbolData.scadaSymbolContent + }; + this.previewMode = false; + } + + onRevertPreviewSettings() { + this.scadaPreviewFormGroup.patchValue({ + scadaSymbolObjectSettings: this.previewScadaSymbolObjectSettings + }, {emitEvent: false}); + this.scadaPreviewFormGroup.markAsPristine(); + } + + onApplyPreviewSettings() { + this.scadaPreviewFormGroup.markAsPristine(); + this.previewScadaSymbolObjectSettings = this.scadaPreviewFormGroup.get('scadaSymbolObjectSettings').value; + this.updatePreviewWidgetSettings(); + } + + tagsUpdated(tags: string[]) { + this.tags = tags; + } + + tagHasStateRenderFunction(tag: string): boolean { + const metadata: ScadaSymbolMetadata = this.scadaSymbolFormGroup.get('metadata').value; + if (metadata.tags) { + const found = metadata.tags.find(t => t.tag === tag); + return !!found?.stateRenderFunction; + } + return false; + } + + tagHasClickAction(tag: string): boolean { + const metadata: ScadaSymbolMetadata = this.scadaSymbolFormGroup.get('metadata').value; + if (metadata.tags) { + const found = metadata.tags.find(t => t.tag === tag); + return !!found?.actions?.click?.actionFunction; + } + return false; + } + + editTagStateRenderFunction(tag: string) { + this.symbolMetadata.editTagStateRenderFunction(tag); + } + + editTagClickAction(tag: string) { + this.symbolMetadata.editTagClickAction(tag); + } + + onSymbolEditObjectDirty(dirty: boolean) { + this.symbolEditorDirty = dirty; + } + + updateScadaSymbol() { + this.dialog.open(UploadImageDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + imageSubType: this.symbolData.imageResource.resourceSubType, + image: this.symbolData.imageResource + } + }).afterClosed().subscribe((result) => { + if (result?.scadaSymbolContent) { + this.symbolData.scadaSymbolContent = result.scadaSymbolContent; + this.symbolEditorData = { + scadaSymbolContent: this.symbolData.scadaSymbolContent + }; + this.symbolEditorDirty = true; + } + }); + } + + downloadScadaSymbol() { + let metadata: ScadaSymbolMetadata; + if (this.scadaSymbolFormGroup.valid) { + metadata = this.scadaSymbolFormGroup.get('metadata').value; + } else { + metadata = parseScadaSymbolMetadataFromContent(this.origSymbolData.scadaSymbolContent); + } + const linkElement = document.createElement('a'); + const scadaSymbolContent = this.prepareScadaSymbolContent(metadata); + const blob = new Blob([scadaSymbolContent], { type: this.symbolData.imageResource.descriptor.mediaType }); + const url = URL.createObjectURL(blob); + linkElement.setAttribute('href', url); + linkElement.setAttribute('download', this.symbolData.imageResource.fileName); + const clickEvent = new MouseEvent('click', + { + view: window, + bubbles: true, + cancelable: false + } + ); + linkElement.dispatchEvent(clickEvent); + } + + createWidget() { + const metadata: ScadaSymbolMetadata = this.scadaSymbolFormGroup.get('metadata').value; + this.dialog.open(SaveWidgetTypeAsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + title: metadata.title, + dialogTitle: 'scada.create-widget-from-symbol', + saveAsActionTitle: 'action.create' + } + }).afterClosed().subscribe( + (saveWidgetAsData) => { + if (saveWidgetAsData) { + this.widgetService.getWidgetType('system.scada_symbol').subscribe( + (widgetTemplate) => { + const symbolUrl = TB_IMAGE_PREFIX + this.symbolData.imageResource.link; + const widget: WidgetTypeDetails = { + image: symbolUrl, + description: metadata.description, + tags: metadata.searchTags, + ...widgetTemplate + }; + widget.fqn = undefined; + widget.id = undefined; + widget.name = saveWidgetAsData.widgetName; + const descriptor = widget.descriptor; + descriptor.sizeX = metadata.widgetSizeX; + descriptor.sizeY = metadata.widgetSizeY; + descriptor.controllerScript = descriptor.controllerScript + .replace(/previewWidth: '\d*px'/gm, `previewWidth: '${metadata.widgetSizeX * 100}px'`); + descriptor.controllerScript = descriptor.controllerScript + .replace(/previewHeight: '\d*px'/gm, `previewHeight: '${metadata.widgetSizeY * 100 + 20}px'`); + const config: WidgetConfig = JSON.parse(descriptor.defaultConfig); + config.title = saveWidgetAsData.widgetName; + config.settings = config.settings || {}; + config.settings.scadaSymbolUrl = symbolUrl; + descriptor.defaultConfig = JSON.stringify(config); + this.widgetService.saveWidgetType(widget).subscribe((saved) => { + if (saveWidgetAsData.widgetBundleId) { + this.widgetService.addWidgetFqnToWidgetBundle(saveWidgetAsData.widgetBundleId, saved.fqn).subscribe(); + } + }); + } + ); + } + } + ); + } + + private updatePreviewWidgetSettings() { + this.previewWidget = deepClone(this.previewWidget); + this.previewWidget.config.settings.scadaSymbolObjectSettings = this.previewScadaSymbolObjectSettings; + this.previewWidgets = [this.previewWidget]; + } + + private prepareScadaSymbolContent(metadata: ScadaSymbolMetadata): string { + const svgContent = this.symbolEditor.getContent(); + return updateScadaSymbolMetadataInContent(svgContent, metadata); + } + + private reset(): void { + if (this.symbolMetadata) { + this.symbolMetadata.selectedOption = 'general'; + } + this.previewMode = false; + } + + private init(data: ScadaSymbolData) { + if (this.authUser.authority === Authority.TENANT_ADMIN) { + this.readonly = data.imageResource.tenantId.id === NULL_UUID; + } else { + this.readonly = this.authUser.authority !== Authority.SYS_ADMIN; + } + this.origSymbolData = data; + this.symbolData = deepClone(data); + this.symbolEditorData = { + scadaSymbolContent: this.symbolData.scadaSymbolContent + }; + const metadata = parseScadaSymbolMetadataFromContent(this.symbolData.scadaSymbolContent); + this.scadaSymbolFormGroup.patchValue({ + metadata + }, {emitEvent: false}); + if (this.readonly) { + this.scadaSymbolFormGroup.disable({emitEvent: false}); + } else { + this.scadaSymbolFormGroup.enable({emitEvent: false}); + } + this.scadaSymbolFormGroup.markAsPristine(); + this.symbolEditorDirty = false; + this.cd.markForCheck(); + } +} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts new file mode 100644 index 0000000000..0955f1b5f9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts @@ -0,0 +1,44 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; +import { ScadaSymbolEditorComponent } from '@home/pages/scada-symbol/scada-symbol-editor.component'; +import { ScadaSymbolTooltipComponentsModule } from '@home/pages/scada-symbol/scada-symbol-tooltip.components'; +import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module'; +import { + ScadaSymbolMetadataComponentsModule +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module'; + +@NgModule({ + declarations: + [ + ScadaSymbolEditorComponent, + ScadaSymbolComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + ScadaSymbolMetadataComponentsModule, + ScadaSymbolTooltipComponentsModule, + WidgetSettingsCommonModule + ] +}) +export class ScadaSymbolModule { } diff --git a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html index cb2b03a845..8d0f6bd6f5 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html @@ -15,9 +15,9 @@ limitations under the License. --> -
+ -

widget.save-widget-as

+

{{ dialogTitle }}

diff --git a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts index e5a0fa3712..3685b553ef 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { Component, OnInit } from '@angular/core'; -import { MatDialogRef } from '@angular/material/dialog'; +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @@ -29,6 +29,12 @@ export interface SaveWidgetTypeAsDialogResult { widgetBundleId?: string; } +export interface SaveWidgetTypeAsDialogData { + dialogTitle?: string; + title?: string; + saveAsActionTitle?: string; +} + @Component({ selector: 'tb-save-widget-type-as-dialog', templateUrl: './save-widget-type-as-dialog.component.html', @@ -39,9 +45,12 @@ export class SaveWidgetTypeAsDialogComponent extends saveWidgetTypeAsFormGroup: FormGroup; bundlesScope: string; + dialogTitle = 'widget.save-widget-as'; + saveAsActionTitle = 'action.saveAs'; constructor(protected store: Store, protected router: Router, + @Inject(MAT_DIALOG_DATA) private data: SaveWidgetTypeAsDialogData, public dialogRef: MatDialogRef, public fb: FormBuilder) { super(store, router, dialogRef); @@ -52,11 +61,18 @@ export class SaveWidgetTypeAsDialogComponent extends } else { this.bundlesScope = 'system'; } + + if (this.data?.dialogTitle) { + this.dialogTitle = this.data.dialogTitle; + } + if (this.data?.saveAsActionTitle) { + this.saveAsActionTitle = this.data.saveAsActionTitle; + } } ngOnInit(): void { this.saveWidgetTypeAsFormGroup = this.fb.group({ - title: [null, [Validators.required]], + title: [this.data?.title, [Validators.required]], widgetsBundle: [null] }); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index 11329df694..19f306fad8 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -257,11 +257,18 @@ [(ngModel)]="widget.tags" (ngModelChange)="isDirty = true"> - - {{ 'widget.deprecated' | translate }} - +
+ + {{ 'widget.scada' | translate }} + + + {{ 'widget.deprecated' | translate }} + +
widget.settings-form-selector - - {{ 'widget.deprecated' | translate }} - +
+ + {{ 'widget.scada' | translate }} + + + {{ 'widget.deprecated' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts index 79dbab63cf..b442fb7482 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts @@ -53,6 +53,7 @@ export class WidgetTypeComponent extends EntityComponent { image: [entity ? entity.image : ''], description: [entity ? entity.description : '', Validators.maxLength(1024)], tags: [entity ? entity.tags : []], + scada: [entity ? entity.scada : false], deprecated: [entity ? entity.deprecated : false] } ); @@ -64,6 +65,7 @@ export class WidgetTypeComponent extends EntityComponent { image: entity.image, description: entity.description, tags: entity.tags, + scada: entity.scada, deprecated: entity.deprecated }); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html index 5e3e4fe6ab..f95d085688 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html @@ -63,6 +63,9 @@ {{descriptionInput.value?.length || 0}}/1024 + + {{ 'widgets-bundle.scada' | translate }} + widgets-bundle.order diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts index 845ece44f0..ca0912ea11 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts @@ -51,6 +51,7 @@ export class WidgetsBundleComponent extends EntityComponent { title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], image: [entity ? entity.image : ''], description: [entity ? entity.description : '', Validators.maxLength(1024)], + scada: [entity ? entity.scada : false], order: [entity ? entity.order : null] } ); @@ -61,6 +62,7 @@ export class WidgetsBundleComponent extends EntityComponent { title: entity.title, image: entity.image, description: entity.description, + scada: entity.scada, order: entity.order }); } diff --git a/ui-ngx/src/app/shared/components/image-input.component.html b/ui-ngx/src/app/shared/components/image-input.component.html index 6a657f70b1..3b7130b634 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.html +++ b/ui-ngx/src/app/shared/components/image-input.component.html @@ -47,7 +47,7 @@ - +
diff --git a/ui-ngx/src/app/shared/components/image-input.component.ts b/ui-ngx/src/app/shared/components/image-input.component.ts index acbff9844f..5bf98c65a9 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.ts +++ b/ui-ngx/src/app/shared/components/image-input.component.ts @@ -53,6 +53,9 @@ import { ImagePipe } from '@shared/pipe/image.pipe'; }) export class ImageInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { + @Input() + accept = 'image/*'; + @Input() label: string; @@ -85,6 +88,9 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, @Input() inputId = this.utils.guid(); + @Input() + allowedExtensions: string; + @Input() @coerceBoolean() processImageApiLink = false; @@ -141,17 +147,19 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, ); return false; } - const reader = new FileReader(); - reader.onload = (_loadEvent) => { - if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { - this.imageUrl = reader.result; - this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); - this.file = file; - this.fileName = fileName; - this.updateModel(); - } - }; - reader.readAsDataURL(file); + if (this.filterFile(flowFile)) { + const reader = new FileReader(); + reader.onload = (_loadEvent) => { + if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { + this.imageUrl = reader.result; + this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); + this.file = file; + this.fileName = fileName; + this.updateModel(); + } + }; + reader.readAsDataURL(file); + } } }); } @@ -198,6 +206,14 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, this.fileNameChanged.emit(this.fileName); } + private filterFile(file: flowjs.FlowFile): boolean { + if (this.allowedExtensions) { + return this.allowedExtensions.split(',').indexOf(file.getExtension()) > -1; + } else { + return true; + } + } + clearImage() { this.imageUrl = null; this.safeImageUrl = null; diff --git a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts index 9b81a0eaba..5894ce9bb4 100644 --- a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts +++ b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts @@ -26,11 +26,15 @@ import { isBase64DataImageUrl, isImageResourceUrl, prependTbImagePrefix, - removeTbImagePrefix + removeTbImagePrefix, + ResourceSubType } from '@shared/models/resource.models'; import { ImageService } from '@core/http/image.service'; import { MatDialog } from '@angular/material/dialog'; -import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component'; +import { + ImageGalleryDialogComponent, + ImageGalleryDialogData +} from '@shared/components/image/image-gallery-dialog.component'; export enum ImageLinkType { none = 'none', @@ -120,21 +124,26 @@ export class GalleryImageInputComponent extends PageComponent implements OnInit, this.detectLinkType(); if (this.linkType === ImageLinkType.resource) { const params = extractParamsFromImageResourceUrl(this.imageUrl); - this.loadingImageResource = true; - this.imageService.getImageInfo(params.type, params.key, {ignoreLoading: true, ignoreErrors: true}).subscribe( - { - next: (res) => { - this.imageResource = res; - this.loadingImageResource = false; - this.cd.markForCheck(); - }, - error: () => { - this.reset(); - this.loadingImageResource = false; - this.cd.markForCheck(); + if (params) { + this.loadingImageResource = true; + this.imageService.getImageInfo(params.type, params.key, {ignoreLoading: true, ignoreErrors: true}).subscribe( + { + next: (res) => { + this.imageResource = res; + this.loadingImageResource = false; + this.cd.markForCheck(); + }, + error: () => { + this.reset(); + this.loadingImageResource = false; + this.cd.markForCheck(); + } } - } - ); + ); + } else { + this.reset(); + this.cd.markForCheck(); + } } else if (this.linkType === ImageLinkType.base64) { this.cd.markForCheck(); } else if (this.linkType === ImageLinkType.external) { @@ -188,11 +197,14 @@ export class GalleryImageInputComponent extends PageComponent implements OnInit, if ($event) { $event.stopPropagation(); } - this.dialog.open(ImageGalleryDialogComponent, { autoFocus: false, disableClose: false, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + imageSubType: ResourceSubType.IMAGE + } }).afterClosed().subscribe((image) => { if (image) { this.linkType = ImageLinkType.resource; diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.ts b/ui-ngx/src/app/shared/components/image/image-dialog.component.ts index 40e7d2ec89..5d3f14612a 100644 --- a/ui-ngx/src/app/shared/components/image/image-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.ts @@ -25,7 +25,7 @@ import { ImageService } from '@core/http/image.service'; import { ImageResource, ImageResourceInfo, imageResourceType } from '@shared/models/resource.models'; import { UploadImageDialogComponent, - UploadImageDialogData + UploadImageDialogData, UploadImageDialogResult } from '@shared/components/image/upload-image-dialog.component'; import { UrlHolder } from '@shared/pipe/image.pipe'; import { ImportExportService } from '@shared/import-export/import-export.service'; @@ -142,19 +142,20 @@ export class ImageDialogComponent extends $event.stopPropagation(); } this.dialog.open(UploadImageDialogComponent, { + UploadImageDialogResult>(UploadImageDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { + imageSubType: this.image.resourceSubType, image: this.image } }).afterClosed().subscribe((result) => { - if (result) { + if (result?.image) { this.imageChanged = true; - this.image = result; + this.image = result.image; let url; - if (result.base64) { - url = result.base64; + if (result.image.base64) { + url = result.image.base64; } else { url = this.image.link; } diff --git a/ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html b/ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html index 8e24e2ffb2..3c4b6b62fd 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html @@ -24,6 +24,7 @@ , protected router: Router, - private imageService: ImageService, - private dialog: MatDialog, + @Inject(MAT_DIALOG_DATA) public data: ImageGalleryDialogData, public dialogRef: MatDialogRef) { super(store, router, dialogRef); } diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.html b/ui-ngx/src/app/shared/components/image/image-gallery.component.html index 21b10e8c66..d9768baf8a 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.html +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.html @@ -22,7 +22,7 @@ [fxShow]="!textSearchMode && (mode === 'grid' || dataSource?.selection.isEmpty())" [ngClass.lt-lg]="{'multi-row': !isSysAdmin}">
- image.gallery + {{ (isScada ? 'scada.symbols' : 'image.gallery') | translate }}
@@ -46,7 +46,7 @@ {{ 'image.include-system-images' | translate }} + (ngModelChange)="includeSystemImagesChanged($event)">{{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }}
@@ -68,7 +68,7 @@ @@ -77,7 +77,7 @@ mat-icon-button color="primary" (click)="uploadImage()" - matTooltip="{{'image.upload-image' | translate }}" + matTooltip="{{ (isScada ? 'scada.upload-symbol' : 'image.upload-image' ) | translate }}" matTooltipPosition="above"> add @@ -87,18 +87,18 @@ mat-flat-button color="primary" (click)="uploadImage()"> - {{ 'image.upload-image' | translate }} + {{ (isScada ? 'scada.upload-symbol' : 'image.upload-image' ) | translate }}
{{ 'image.include-system-images' | translate }} + (ngModelChange)="includeSystemImagesChanged($event)">{{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }}
@@ -106,7 +106,7 @@   + placeholder="{{ (isScada ? 'scada.search' : 'image.search' ) | translate }}"/> - -
@@ -318,7 +318,7 @@
-
image.no-images
+
{{ (isScada ? 'scada.no-symbols' : 'image.no-images') | translate }}
@@ -326,18 +326,18 @@
- +
+
+ +
+
+
+ + +
{{ (disabled ? 'scada.no-symbol' : 'scada.no-symbol-selected') | translate }}
+
+ + + diff --git a/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.scss b/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.scss new file mode 100644 index 0000000000..2392f8185d --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.scss @@ -0,0 +1,149 @@ +/** + * 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 "../../../../scss/constants"; + +$containerHeight: 96px !default; + +:host { + .tb-container { + margin-top: 0; + padding: 0 0 16px; + display: flex; + flex-direction: column; + gap: 8px; + label.tb-title { + display: block; + padding-bottom: 0; + } + } + + .tb-scada-symbol-select-container { + width: 100%; + height: $containerHeight; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + align-items: center; + &.disabled { + width: $containerHeight; + .tb-scada-symbol-container { + width: $containerHeight - 2px; + border-right: none; + } + } + } + + .tb-scada-symbol-container { + width: $containerHeight - 1px; + height: $containerHeight - 2px; + padding: 8px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 4px; + border-right: 1px solid rgba(0, 0, 0, 0.12); + background: #fff; + overflow: hidden; + } + + .tb-scada-symbol-preview { + width: auto; + max-width: $containerHeight - 2px; + height: auto; + max-height: $containerHeight - 2px; + } + + .tb-no-symbol { + text-align: center; + color: rgba(0, 0, 0, 0.38); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + } + + .tb-scada-symbol-info-container { + display: flex; + flex: 1; + align-self: stretch; + padding: 0 8px; + justify-content: flex-end; + align-items: center; + gap: 4px; + .tb-resource-scada-symbol-container { + padding: 8px; + display: flex; + flex: 1; + align-self: stretch; + justify-content: center; + align-items: flex-start; + flex-direction: column; + gap: 4px; + .tb-scada-symbol-title { + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + } + &.loading { + align-items: center; + } + } + + .tb-scada-symbol-clear-btn { + color: rgba(0,0,0,0.38); + } + } + + .tb-scada-symbol-select-buttons-container { + display: flex; + flex: 1; + align-self: stretch; + padding: 8px; + gap: 8px; + justify-content: center; + align-items: flex-start; + .tb-scada-symbol-select-button { + width: 100%; + height: 100%; + align-self: stretch; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; + padding: 8px; + line-height: normal; + font-size: 12px; + @media #{$mat-gt-xs} { + padding: 16px; + } + .mat-icon { + width: 24px; + height: 24px; + font-size: 24px; + margin: 0; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.ts b/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.ts new file mode 100644 index 0000000000..78aca19ce5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/scada-symbol-input.component.ts @@ -0,0 +1,198 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { + extractParamsFromImageResourceUrl, + IMAGE_BASE64_URL_PREFIX, + ImageResourceInfo, + prependTbImagePrefix, + removeTbImagePrefix, + ResourceSubType +} from '@shared/models/resource.models'; +import { ImageService } from '@core/http/image.service'; +import { MatDialog } from '@angular/material/dialog'; +import { + ImageGalleryDialogComponent, + ImageGalleryDialogData +} from '@shared/components/image/image-gallery-dialog.component'; +import { ScadaSymbolMetadata } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { stringToBase64 } from '@core/utils'; + +export enum ScadaSymbolLinkType { + none = 'none', + content = 'content', + resource = 'resource' +} + +@Component({ + selector: 'tb-scada-symbol-input', + templateUrl: './scada-symbol-input.component.html', + styleUrls: ['./scada-symbol-input.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolInputComponent), + multi: true + } + ] +}) +export class ScadaSymbolInputComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor { + + @Input() + label: string; + + @Input() + @coerceBoolean() + required = false; + + @Input() + disabled: boolean; + + @Input() + scadaSymbolContent: string; + + @Input() + scadaSymbolMetadata: ScadaSymbolMetadata; + + scadaSymbolUrl: string; + + imageResource: ImageResourceInfo; + + loadingImageResource = false; + + ScadaSymbolLinkType = ScadaSymbolLinkType; + + linkType: ScadaSymbolLinkType = ScadaSymbolLinkType.none; + + private propagateChange = null; + + constructor(protected store: Store, + private imageService: ImageService, + private dialog: MatDialog, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit() { + if (this.scadaSymbolContent && this.scadaSymbolMetadata) { + this.scadaSymbolUrl = IMAGE_BASE64_URL_PREFIX + 'svg+xml;base64,' + stringToBase64(this.scadaSymbolContent); + this.linkType = ScadaSymbolLinkType.content; + } + } + + ngOnDestroy() { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.detectLinkType(); + } + } + + writeValue(value: string): void { + value = removeTbImagePrefix(value); + if (this.scadaSymbolUrl !== value) { + this.reset(); + this.scadaSymbolUrl = value; + this.detectLinkType(); + if (this.linkType === ScadaSymbolLinkType.resource) { + const params = extractParamsFromImageResourceUrl(this.scadaSymbolUrl); + if (params) { + this.loadingImageResource = true; + this.imageService.getImageInfo(params.type, params.key, {ignoreLoading: true, ignoreErrors: true}).subscribe( + { + next: (res) => { + this.imageResource = res; + this.loadingImageResource = false; + this.cd.markForCheck(); + }, + error: () => { + this.reset(); + this.loadingImageResource = false; + this.cd.markForCheck(); + } + } + ); + } else { + this.reset(); + this.cd.markForCheck(); + } + } + } + } + + private detectLinkType() { + if (this.scadaSymbolUrl) { + this.linkType = ScadaSymbolLinkType.resource; + } else { + this.linkType = ScadaSymbolLinkType.none; + } + } + + private updateModel(value: string) { + this.cd.markForCheck(); + if (this.scadaSymbolUrl !== value) { + this.scadaSymbolUrl = value; + this.propagateChange(prependTbImagePrefix(this.scadaSymbolUrl)); + } + } + + private reset() { + this.linkType = ScadaSymbolLinkType.none; + this.imageResource = null; + } + + clearSymbol() { + this.reset(); + this.updateModel(null); + } + + openGallery($event: Event): void { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(ImageGalleryDialogComponent, { + autoFocus: false, + disableClose: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + imageSubType: ResourceSubType.SCADA_SYMBOL + } + }).afterClosed().subscribe((image) => { + if (image) { + this.linkType = ScadaSymbolLinkType.resource; + this.imageResource = image; + this.updateModel(image.link); + } + }); + } + +} diff --git a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html index 5255234ba8..6f928639db 100644 --- a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html @@ -17,7 +17,7 @@ -->
-

{{ ( uploadImage ? 'image.upload-image' : 'image.update-image' ) | translate }}

+

{{ ( uploadImage ? (isScada ? 'scada.upload-symbol' : 'image.upload-image') : (isScada ? 'scada.update-symbol' : 'image.update-image') ) | translate }}